package service

import (
	"bytes"
	"context"
	"crypto/md5" // nolint:gosec // used only for tests
	"crypto/x509"
	_ "embed"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"testing"
	"time"

	shared_mdm "github.com/fleetdm/fleet/v4/pkg/mdm"
	"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
	"github.com/fleetdm/fleet/v4/pkg/optjson"
	"github.com/fleetdm/fleet/v4/server/datastore/mysql"
	"github.com/fleetdm/fleet/v4/server/fleet"
	servermdm "github.com/fleetdm/fleet/v4/server/mdm"
	apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
	"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
	microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
	"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
	"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
	"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
	scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
	"github.com/fleetdm/fleet/v4/server/ptr"
	"github.com/fleetdm/fleet/v4/server/service/contract"
	"github.com/fleetdm/fleet/v4/server/service/integrationtest/scep_server"
	"github.com/fleetdm/fleet/v4/server/test"
	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
	micromdm "github.com/micromdm/micromdm/mdm/mdm"
	"github.com/micromdm/plist"
	"github.com/smallstep/pkcs7"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func (s *integrationMDMTestSuite) signedProfilesMatch(want, got [][]byte) {
	t := s.T()
	rootCA := x509.NewCertPool()

	assets, err := s.ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{
		fleet.MDMAssetCACert,
	}, nil)
	require.NoError(t, err)

	require.True(t, rootCA.AppendCertsFromPEM(assets[fleet.MDMAssetCACert].Value))

	// verify that all the profiles were signed usign the SCEP certificate,
	// and grab their contents
	signedContents := [][]byte{}
	for _, prof := range got {
		p7, err := pkcs7.Parse(prof)
		require.NoError(t, err)
		require.NoError(t, p7.VerifyWithChain(rootCA))
		signedContents = append(signedContents, p7.Content)
	}

	// verify that contents match
	require.ElementsMatch(t, want, signedContents)
}

func (s *integrationMDMTestSuite) TestAppleProfileManagement() {
	t := s.T()
	ctx := context.Background()

	err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
	require.NoError(t, err)

	globalProfiles := [][]byte{
		mobileconfigForTest("N1", "I1"),
		mobileconfigForTest("N2", "I2"),
	}
	wantGlobalProfiles := globalProfiles
	wantGlobalProfiles = append(
		wantGlobalProfiles,
		setupExpectedFleetdProfile(t, s.server.URL, t.Name(), nil),
	)

	// add global profiles
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)

	// invalid secrets
	invalidSecretsProfile := []byte(`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PayloadContent</key>
	<array/>
	<key>PayloadDisplayName</key>
	<string>My profile</string>
	<key>PayloadIdentifier</key>
	<string>$FLEET_SECRET_INVALID</string>
	<key>PayloadType</key>
	<string>Configuration</string>
	<key>PayloadUUID</key>
	<string>601E0B42-0989-4FAD-A61B-18656BA3670E</string>
	<key>PayloadVersion</key>
	<integer>1</integer>
</dict>
</plist>
`)

	res := s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{invalidSecretsProfile}}, http.StatusUnprocessableEntity)
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "$FLEET_SECRET_INVALID")

	invalidSecretsProfile = []byte(`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PayloadContent</key>
	<array/>
	<key>PayloadDisplayName</key>
	<string>$FLEET_SECRET_INVALID</string>
	<key>PayloadIdentifier</key>
	<string>N3</string>
	<key>PayloadType</key>
	<string>Configuration</string>
	<key>PayloadUUID</key>
	<string>601E0B42-0989-4FAD-A61B-18656BA3670E</string>
	<key>PayloadVersion</key>
	<integer>1</integer>
</dict>
</plist>
`)

	res = s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{invalidSecretsProfile}}, http.StatusUnprocessableEntity)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "PayloadDisplayName cannot contain FLEET_SECRET variables")

	// create a new team
	tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"})
	require.NoError(t, err)

	// add an enroll secret so the fleetd profiles differ
	var teamResp teamEnrollSecretsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/secrets", tm.ID),
		modifyTeamEnrollSecretsRequest{
			Secrets: []fleet.EnrollSecret{{Secret: "team1_enroll_sec"}},
		}, http.StatusOK, &teamResp)

	teamProfiles := [][]byte{
		mobileconfigForTest("N3", "I3"),
	}
	wantTeamProfiles := teamProfiles
	wantTeamProfiles = append(
		wantTeamProfiles,
		setupExpectedFleetdProfile(t, s.server.URL, "team1_enroll_sec", &tm.ID),
	)
	// add profiles to the team
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: teamProfiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm.ID))

	// create a non-macOS host
	_, err = s.ds.NewHost(context.Background(), &fleet.Host{
		ID:            1,
		OsqueryHostID: ptr.String("non-macos-host"),
		NodeKey:       ptr.String("non-macos-host"),
		UUID:          uuid.New().String(),
		Hostname:      fmt.Sprintf("%sfoo.local.non.macos", t.Name()),
		Platform:      "windows",
	})
	require.NoError(t, err)

	// create a host that's not enrolled into MDM
	_, err = s.ds.NewHost(context.Background(), &fleet.Host{
		ID:            2,
		OsqueryHostID: ptr.String("not-mdm-enrolled"),
		NodeKey:       ptr.String("not-mdm-enrolled"),
		UUID:          uuid.New().String(),
		Hostname:      fmt.Sprintf("%sfoo.local.not.enrolled", t.Name()),
		Platform:      "darwin",
	})
	require.NoError(t, err)

	// Create a host and then enroll to MDM.
	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setupPusher(s, t, mdmDevice)

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	installs, removes := checkNextPayloads(t, mdmDevice, false)
	// verify that we received all profiles
	s.signedProfilesMatch(
		append(wantGlobalProfiles, setupExpectedCAProfile(t, s.ds)),
		installs,
	)
	require.Empty(t, removes)

	expectedNoTeamSummary := fleet.MDMProfilesSummary{
		Pending:   0,
		Failed:    0,
		Verifying: 1,
		Verified:  0,
	}
	expectedTeamSummary := fleet.MDMProfilesSummary{}
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamSummary, &expectedNoTeamSummary)
	s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary) // empty because no hosts in team

	// add the host to a team
	err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{host.ID}))
	require.NoError(t, err)

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	// verify that we should install the team profile
	s.signedProfilesMatch(wantTeamProfiles, installs)
	// verify that we should delete both profiles
	require.ElementsMatch(t, []string{"I1", "I2"}, removes)

	expectedNoTeamSummary = fleet.MDMProfilesSummary{}
	expectedTeamSummary = fleet.MDMProfilesSummary{
		Pending:   0,
		Failed:    0,
		Verifying: 1,
		Verified:  0,
	}
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamSummary, &expectedNoTeamSummary) // empty because host was transferred
	s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary)  // host now verifying team profiles

	// Use secret variables in a profile
	secretIdentifier := "secret-identifier-1"
	secretType := "secret.type.1"
	secretProfile := string(mobileconfigForTest("NS1", "IS1"))
	req := createSecretVariablesRequest{
		SecretVariables: []fleet.SecretVariable{
			{
				Name:  "FLEET_SECRET_IDENTIFIER",
				Value: secretIdentifier,
			},
			{
				Name:  "FLEET_SECRET_TYPE",
				Value: secretType,
			},
			{
				Name:  "FLEET_SECRET_PROFILE",
				Value: secretProfile,
			},
		},
	}
	secretResp := createSecretVariablesResponse{}
	s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp)

	// set new team profiles (delete + addition)
	teamProfiles = [][]byte{
		mobileconfigForTest("N4", "I4"),
		mobileconfigForTestWithContent("N5", "I5", "$FLEET_SECRET_IDENTIFIER", "${FLEET_SECRET_TYPE}",
			"InnerName5"),
		// The whole profile is one big secret.
		[]byte("$FLEET_SECRET_PROFILE"),
	}
	// We deep copy one of the team profiles because we will modify the slice in place, and we want to keep the originals for later.
	wantTeamProfiles = [][]byte{
		teamProfiles[0],
		make([]byte, len(teamProfiles[1])),
		{},
	}
	copy(wantTeamProfiles[1], teamProfiles[1])
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: teamProfiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm.ID))

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	// Manually replace the expected secret variables in the profile
	wantTeamProfiles[1] = []byte(strings.ReplaceAll(string(wantTeamProfiles[1]), "$FLEET_SECRET_IDENTIFIER", secretIdentifier))
	wantTeamProfiles[1] = []byte(strings.ReplaceAll(string(wantTeamProfiles[1]), "${FLEET_SECRET_TYPE}", secretType))
	wantTeamProfiles[2] = []byte(secretProfile)
	// verify that we should install the team profiles
	s.signedProfilesMatch(wantTeamProfiles, installs)
	// verify that we should delete the old team profiles
	require.ElementsMatch(t, []string{"I3"}, removes)

	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamSummary, &expectedNoTeamSummary) // empty because host was transferred
	s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary)  // host still verifying team profiles

	// Upload the same profiles again. No changes expected.
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: teamProfiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm.ID))
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	require.Empty(t, installs)
	require.Empty(t, removes)

	// Change the secret variable and upload the profiles again. We should see the profile with updated secret installed.
	secretType = "new.secret.type.1"
	req = createSecretVariablesRequest{
		SecretVariables: []fleet.SecretVariable{
			{
				Name:  "FLEET_SECRET_IDENTIFIER",
				Value: secretIdentifier, // did not change
			},
			{
				Name:  "FLEET_SECRET_TYPE",
				Value: secretType, // changed
			},
			{
				Name:  "FLEET_SECRET_PROFILE",
				Value: secretProfile, // did not change
			},
		},
	}
	s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp)
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: teamProfiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm.ID))
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	// Manually replace the expected secret variables in the profile
	wantTeamProfilesChanged := [][]byte{
		teamProfiles[1],
	}
	wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "$FLEET_SECRET_IDENTIFIER",
		secretIdentifier))
	wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "${FLEET_SECRET_TYPE}", secretType))
	// verify that we should install the team profiles
	s.signedProfilesMatch(wantTeamProfilesChanged, installs)
	wantTeamProfiles[1] = wantTeamProfilesChanged[0]
	// No profiles should be deleted
	assert.Empty(t, removes)

	// Clear the profiles using the new (non-deprecated) endpoint.
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: nil}, http.StatusNoContent, "team_id",
		fmt.Sprint(tm.ID), "dry_run", "true")
	s.assertConfigProfilesByIdentifier(&tm.ID, "IS1", true)
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: nil}, http.StatusNoContent, "team_id",
		fmt.Sprint(tm.ID), "dry_run", "false")
	s.assertConfigProfilesByIdentifier(&tm.ID, "IS1", false)
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	require.Empty(t, installs)
	assert.Len(t, removes, 3)

	// And reapply the same profiles using the new (non-deprecated) endpoint.
	batchRequest := batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N4", Contents: teamProfiles[0]},
		{Name: "N5", Contents: teamProfiles[1]},
		{Name: "NS1", Contents: teamProfiles[2]},
	}}
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID), "dry_run", "true")
	s.assertConfigProfilesByIdentifier(&tm.ID, "I4", false)
	s.assertConfigProfilesByIdentifier(&tm.ID, "I5", false)
	s.assertConfigProfilesByIdentifier(&tm.ID, "IS1", false)
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID))
	s.assertConfigProfilesByIdentifier(&tm.ID, "IS1", true)
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	assert.Empty(t, removes)
	// verify that we should install the team profiles
	s.signedProfilesMatch(wantTeamProfiles, installs)

	// Upload the same profiles again. No changes expected.
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID), "dry_run", "true")
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID))
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	require.Empty(t, installs)
	require.Empty(t, removes)

	// Change the secret variable and upload the profiles again. We should see the profile with updated secret installed.
	secretType = "new2.secret.type.1"
	req = createSecretVariablesRequest{
		SecretVariables: []fleet.SecretVariable{
			{
				Name:  "FLEET_SECRET_IDENTIFIER",
				Value: secretIdentifier, // did not change
			},
			{
				Name:  "FLEET_SECRET_TYPE",
				Value: secretType, // changed
			},
			{
				Name:  "FLEET_SECRET_PROFILE",
				Value: secretProfile, // did not change
			},
		},
	}
	s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp)
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID), "dry_run", "true")
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID))
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	// Manually replace the expected secret variables in the profile
	wantTeamProfilesChanged = [][]byte{
		teamProfiles[1],
	}
	wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "$FLEET_SECRET_IDENTIFIER",
		secretIdentifier))
	wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "${FLEET_SECRET_TYPE}", secretType))
	// verify that we should install the team profiles
	s.signedProfilesMatch(wantTeamProfilesChanged, installs)
	wantTeamProfiles[1] = wantTeamProfilesChanged[0]
	// No profiles should be deleted
	assert.Empty(t, removes)

	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", host.ID), getHostRequest{}, http.StatusOK, &hostResp)
	require.NotEmpty(t, hostResp.Host.MDM.Profiles)
	resProfiles := *hostResp.Host.MDM.Profiles
	// two extra profiles: fleetd config and root CA
	require.Len(t, resProfiles, len(wantTeamProfiles)+2)

	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamSummary, &expectedNoTeamSummary) // empty because host was transferred
	s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary)  // host still verifying team profiles

	// add a new profile to the team
	mcUUID := "a" + uuid.NewString()
	prof := mcBytesForTest("name-"+mcUUID, "identifier-"+mcUUID, mcUUID)
	wantTeamProfiles = append(wantTeamProfiles, prof)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `INSERT INTO mdm_apple_configuration_profiles (profile_uuid, team_id, name, identifier, mobileconfig, checksum, uploaded_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);`
		_, err := q.ExecContext(context.Background(), stmt, mcUUID, tm.ID, "name-"+mcUUID, "identifier-"+mcUUID, prof, test.MakeTestBytes())
		return err
	})
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	require.Len(t, installs, 1)
	s.signedProfilesMatch([][]byte{prof}, installs)
	require.Empty(t, removes)
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{Verifying: 1}, nil)

	// can't resend profile while verifying
	res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, mcUUID), nil, http.StatusConflict)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Couldn’t resend. Configuration profiles with “pending” or “verifying” status can’t be resent.")

	// set the profile to pending, can't resend
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE host_mdm_apple_profiles SET status = ? WHERE profile_uuid = ? AND host_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, fleet.MDMDeliveryPending, mcUUID, host.UUID)
		return err
	})
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{Pending: 1}, nil)
	res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, mcUUID), nil, http.StatusConflict)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Couldn’t resend. Configuration profiles with “pending” or “verifying” status can’t be resent.")

	// set the profile to failed, can resend
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE host_mdm_apple_profiles SET status = ? WHERE profile_uuid = ? AND host_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, fleet.MDMDeliveryFailed, mcUUID, host.UUID)
		return err
	})
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{Failed: 1}, nil)
	_ = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, mcUUID), nil, http.StatusAccepted)
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	require.Len(t, installs, 1)
	s.signedProfilesMatch([][]byte{prof}, installs)
	require.Empty(t, removes)
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{Verifying: 1}, nil)

	// set the profile to failed, can resend from device endpoint
	token := "good_token"
	updateDeviceTokenForHost(t, s.ds, host.ID, token)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE host_mdm_apple_profiles SET status = ? WHERE profile_uuid = ? AND host_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, fleet.MDMDeliveryFailed, mcUUID, host.UUID)
		return err
	})
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{Failed: 1}, nil)
	_ = s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/configuration_profiles/%s/resend", token, mcUUID), nil, http.StatusAccepted)
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	require.Len(t, installs, 1)
	s.signedProfilesMatch([][]byte{prof}, installs)
	require.Empty(t, removes)
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{Verifying: 1}, nil)

	// can't resend profile while verifying
	res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, mcUUID), nil, http.StatusConflict)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Couldn’t resend. Configuration profiles with “pending” or “verifying” status can’t be resent.")

	// set the profile to verified, can resend
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE host_mdm_apple_profiles SET status = ? WHERE profile_uuid = ? AND host_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, fleet.MDMDeliveryVerified, mcUUID, host.UUID)
		return err
	})
	_ = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, mcUUID), nil, http.StatusAccepted)
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	require.Len(t, installs, 1)
	s.signedProfilesMatch([][]byte{prof}, installs)
	require.Empty(t, removes)
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{Verifying: 1}, nil)
	s.lastActivityMatches(
		fleet.ActivityTypeResentConfigurationProfile{}.ActivityName(),
		fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "profile_name": %q}`, host.ID, host.DisplayName(), "name-"+mcUUID),
		0)

	// add a declaration to the team
	declIdent := "decl-ident-" + uuid.NewString()
	fields := map[string][]string{
		"team_id": {fmt.Sprintf("%d", tm.ID)},
	}
	body, headers := generateNewProfileMultipartRequest(
		t, "some-declaration.json", declarationForTest(declIdent), s.token, fields,
	)
	res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusOK, headers)
	var resp newMDMConfigProfileResponse
	err = json.NewDecoder(res.Body).Decode(&resp)
	require.NoError(t, err)
	require.NotEmpty(t, resp.ProfileUUID)
	require.Equal(t, "d", string(resp.ProfileUUID[0]))
	declUUID := resp.ProfileUUID

	checkDDMSync := func(d *mdmtest.TestAppleMDMClient) {
		require.NoError(t, ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger))
		cmd, err := d.Idle()
		require.NoError(t, err)
		require.NotNil(t, cmd)
		require.Equal(t, "DeclarativeManagement", cmd.Command.RequestType)
		cmd, err = d.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
		require.Nil(t, cmd, fmt.Sprintf("expected no more commands, but got: %+v", cmd))
		_, err = d.DeclarativeManagement("tokens")
		require.NoError(t, err)
	}
	checkDDMSync(mdmDevice)
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{Verifying: 1}, nil)

	// can not resend declarations as admin or from device endpoint
	res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, declUUID), nil, http.StatusBadRequest)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, fleet.CantResendAppleDeclarationProfilesMessage)
	res = s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/configuration_profiles/%s/resend", token, declUUID), nil, http.StatusBadRequest)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, fleet.CantResendAppleDeclarationProfilesMessage)

	// set the declaration to verified
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE host_mdm_apple_declarations SET status = ? WHERE declaration_uuid = ? AND host_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, fleet.MDMDeliveryVerified, declUUID, host.UUID)
		return err
	})

	// transfer the host to the global team
	err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(nil, []uint{host.ID}))
	require.NoError(t, err)

	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice, false)
	require.Len(t, installs, len(wantGlobalProfiles))
	s.signedProfilesMatch(wantGlobalProfiles, installs)
	require.Len(t, removes, len(wantTeamProfiles))
	expectedNoTeamSummary = fleet.MDMProfilesSummary{Pending: 1}
	expectedTeamSummary = fleet.MDMProfilesSummary{}
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamSummary, &expectedNoTeamSummary) // host now removing team profiles
	s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary)

	// can't resend profile from another team
	res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, mcUUID), nil, http.StatusNotFound)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Unable to match profile to host")

	// invalid profile UUID prefix should return 404
	res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, "z"+uuid.NewString()), nil, http.StatusNotFound)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Invalid profile UUID prefix")

	// set OS updates settings for no-team and team, should not change the
	// summaries as this profile is ignored.
	s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"macos_updates": {
				"deadline": "2023-12-31",
				"minimum_version": "13.3.7"
			}
		}
	}`), http.StatusOK)
	s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID), fleet.TeamPayload{
		MDM: &fleet.TeamPayloadMDM{
			MacOSUpdates: &fleet.AppleOSUpdateSettings{
				Deadline:       optjson.SetString("1992-01-01"),
				MinimumVersion: optjson.SetString("13.1.1"),
			},
		},
	}, http.StatusOK)
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamSummary, &expectedNoTeamSummary)
	s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary)

	// it should also not show up in the host's profiles list
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", host.ID), getHostRequest{}, http.StatusOK, &hostResp)
	require.NotEmpty(t, hostResp.Host.MDM.Profiles)
	resProfiles = *hostResp.Host.MDM.Profiles
	// two extra profiles: fleetd config and root CA
	require.Len(t, resProfiles, len(wantGlobalProfiles)+2)
}

func (s *integrationMDMTestSuite) TestAppleProfileRetries() {
	t := s.T()
	ctx := context.Background()

	enrollSecret := "test-profile-retries-secret"
	err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: enrollSecret}})
	require.NoError(t, err)

	testProfiles := [][]byte{
		mobileconfigForTest("N1", "I1"),
		mobileconfigForTest("N2", "I2"),
	}
	initialExpectedProfiles := testProfiles
	initialExpectedProfiles = append(
		initialExpectedProfiles,
		setupExpectedFleetdProfile(t, s.server.URL, enrollSecret, nil),
		setupExpectedCAProfile(t, s.ds),
	)

	h, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setupPusher(s, t, mdmDevice)

	expectedProfileStatuses := map[string]fleet.MDMDeliveryStatus{
		"I1": fleet.MDMDeliveryVerifying,
		"I2": fleet.MDMDeliveryVerifying,
		mobileconfig.FleetdConfigPayloadIdentifier:      fleet.MDMDeliveryVerifying,
		mobileconfig.FleetCARootConfigPayloadIdentifier: fleet.MDMDeliveryVerifying,
	}
	checkProfilesStatus := func(t *testing.T) {
		storedProfs, err := s.ds.GetHostMDMAppleProfiles(ctx, h.UUID)
		require.NoError(t, err)
		require.Len(t, storedProfs, len(expectedProfileStatuses))
		for _, p := range storedProfs {
			want, ok := expectedProfileStatuses[p.Identifier]
			require.True(t, ok, "unexpected profile: %s", p.Identifier)
			require.Equal(t, want, *p.Status, "expected status %s but got %s for profile: %s", want, *p.Status, p.Identifier)
		}
	}

	expectedRetryCounts := map[string]uint{
		"I1": 0,
		"I2": 0,
		mobileconfig.FleetdConfigPayloadIdentifier:      0,
		mobileconfig.FleetCARootConfigPayloadIdentifier: 0,
	}
	checkRetryCounts := func(t *testing.T) {
		counts, err := s.ds.GetHostMDMProfilesRetryCounts(ctx, h)
		require.NoError(t, err)
		require.Len(t, counts, len(expectedRetryCounts))
		for _, c := range counts {
			want, ok := expectedRetryCounts[c.ProfileIdentifier]
			require.True(t, ok, "unexpected profile: %s", c.ProfileIdentifier)
			require.Equal(t, want, c.Retries, "expected retry count %d but got %d for profile: %s", want, c.Retries, c.ProfileIdentifier)
		}
	}

	hostProfsByIdent := map[string]*fleet.HostMacOSProfile{
		"I1": {
			Identifier:  "I1",
			DisplayName: "N1",
			InstallDate: time.Now().Add(15 * time.Minute),
		},
		"I2": {
			Identifier:  "I2",
			DisplayName: "N2",
			InstallDate: time.Now().Add(15 * time.Minute),
		},
		mobileconfig.FleetdConfigPayloadIdentifier: {
			Identifier:  mobileconfig.FleetdConfigPayloadIdentifier,
			DisplayName: "Fleetd configuration",
			InstallDate: time.Now().Add(15 * time.Minute),
		},
	}
	reportHostProfs := func(t *testing.T, identifiers ...string) {
		report := make(map[string]*fleet.HostMacOSProfile, len(hostProfsByIdent))
		for _, ident := range identifiers {
			report[ident] = hostProfsByIdent[ident]
		}
		require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, s.ds, h, report))
	}

	setProfileUploadedAt := func(t *testing.T, uploadedAt time.Time, identifiers ...interface{}) {
		bindVars := strings.TrimSuffix(strings.Repeat("?, ", len(identifiers)), ", ")
		stmt := fmt.Sprintf("UPDATE mdm_apple_configuration_profiles SET uploaded_at = ? WHERE identifier IN(%s)", bindVars)
		args := append([]interface{}{uploadedAt}, identifiers...)
		mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
			_, err := tx.ExecContext(ctx, stmt, args...)
			return err
		})
	}

	t.Run("retry after verifying", func(t *testing.T) {
		// upload test profiles then simulate expired grace period by setting updated_at timestamp of profiles back by 48 hours
		s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		setProfileUploadedAt(t, time.Now().Add(-48*time.Hour), "I1", "I2", mobileconfig.FleetdConfigPayloadIdentifier)

		// trigger initial profile sync and confirm that we received all profiles
		s.awaitTriggerProfileSchedule(t)
		installs, removes := checkNextPayloads(t, mdmDevice, false)
		s.signedProfilesMatch(initialExpectedProfiles, installs)
		require.Empty(t, removes)

		checkProfilesStatus(t) // all profiles verifying
		checkRetryCounts(t)    // no retries yet

		// report osquery results with I2 missing and confirm I2 marked as pending and other profiles are marked as verified
		reportHostProfs(t, "I1", mobileconfig.FleetdConfigPayloadIdentifier)
		expectedProfileStatuses["I2"] = fleet.MDMDeliveryPending
		expectedProfileStatuses["I1"] = fleet.MDMDeliveryVerified
		expectedProfileStatuses[mobileconfig.FleetdConfigPayloadIdentifier] = fleet.MDMDeliveryVerified
		checkProfilesStatus(t)
		expectedRetryCounts["I2"] = 1
		checkRetryCounts(t)

		// trigger a profile sync and confirm that the install profile command for I2 was resent
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, false)
		s.signedProfilesMatch([][]byte{initialExpectedProfiles[1]}, installs)
		require.Empty(t, removes)

		// report osquery results with I2 present and confirm that all profiles are verified
		reportHostProfs(t, "I1", "I2", mobileconfig.FleetdConfigPayloadIdentifier)
		expectedProfileStatuses["I2"] = fleet.MDMDeliveryVerified
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that no profiles were sent
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, false)
		require.Empty(t, installs)
		require.Empty(t, removes)
	})

	t.Run("retry after verification", func(t *testing.T) {
		// report osquery results with I1 missing and confirm that the I1 marked as pending (initial retry)
		reportHostProfs(t, "I2", mobileconfig.FleetdConfigPayloadIdentifier)
		expectedProfileStatuses["I1"] = fleet.MDMDeliveryPending
		checkProfilesStatus(t)
		expectedRetryCounts["I1"] = 1
		checkRetryCounts(t)

		// trigger a profile sync and confirm that the install profile command for I1 was resent
		s.awaitTriggerProfileSchedule(t)
		installs, removes := checkNextPayloads(t, mdmDevice, false)
		s.signedProfilesMatch([][]byte{initialExpectedProfiles[0]}, installs)
		require.Empty(t, removes)

		// report osquery results with I1 missing again and confirm that the I1 marked as failed (max retries exceeded)
		reportHostProfs(t, "I2", mobileconfig.FleetdConfigPayloadIdentifier)
		expectedProfileStatuses["I1"] = fleet.MDMDeliveryFailed
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile command for I1 was not resent
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, false)
		require.Empty(t, installs)
		require.Empty(t, removes)
	})

	t.Run("retry after device error", func(t *testing.T) {
		// add another profile and set the updated_at timestamp back by 48 hours
		newProfile := mobileconfigForTest("N3", "I3")
		testProfiles = append(testProfiles, newProfile)
		s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		setProfileUploadedAt(t, time.Now().Add(-48*time.Hour), "I1", "I2", mobileconfig.FleetdConfigPayloadIdentifier, "I3")

		// trigger a profile sync and confirm that the install profile command for I3 was sent and
		// simulate a device error
		s.awaitTriggerProfileSchedule(t)
		installs, removes := checkNextPayloads(t, mdmDevice, true)
		s.signedProfilesMatch([][]byte{newProfile}, installs)
		require.Empty(t, removes)
		expectedProfileStatuses["I3"] = fleet.MDMDeliveryPending
		checkProfilesStatus(t)
		expectedRetryCounts["I3"] = 1
		checkRetryCounts(t)

		// trigger a profile sync and confirm that the install profile command for I3 was sent and
		// simulate a device ack
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, false)
		s.signedProfilesMatch([][]byte{newProfile}, installs)
		require.Empty(t, removes)
		expectedProfileStatuses["I3"] = fleet.MDMDeliveryVerifying
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// report osquery results with I3 missing and confirm that the I3 marked as failed (max
		// retries exceeded)
		reportHostProfs(t, "I2", mobileconfig.FleetdConfigPayloadIdentifier)
		expectedProfileStatuses["I3"] = fleet.MDMDeliveryFailed
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile command for I3 was not resent
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, false)
		require.Empty(t, installs)
		require.Empty(t, removes)
	})

	t.Run("repeated device error", func(t *testing.T) {
		// add another profile and set the updated_at timestamp back by 48 hours
		newProfile := mobileconfigForTest("N4", "I4")
		testProfiles = append(testProfiles, newProfile)
		s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		setProfileUploadedAt(t, time.Now().Add(-48*time.Hour), "I1", "I2", mobileconfig.FleetdConfigPayloadIdentifier, "I3", "I4")

		// trigger a profile sync and confirm that the install profile command for I3 was sent and
		// simulate a device error
		s.awaitTriggerProfileSchedule(t)
		installs, removes := checkNextPayloads(t, mdmDevice, true)
		s.signedProfilesMatch([][]byte{newProfile}, installs)
		require.Empty(t, removes)
		expectedProfileStatuses["I4"] = fleet.MDMDeliveryPending
		checkProfilesStatus(t)
		expectedRetryCounts["I4"] = 1
		checkRetryCounts(t)

		// trigger a profile sync and confirm that the install profile command for I4 was sent and
		// simulate a second device error
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, true)
		s.signedProfilesMatch([][]byte{newProfile}, installs)
		require.Empty(t, removes)
		expectedProfileStatuses["I4"] = fleet.MDMDeliveryFailed
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile command for I3 was not resent
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, false)
		require.Empty(t, installs)
		require.Empty(t, removes)
	})

	t.Run("retry count does not reset", func(t *testing.T) {
		// add another profile and set the updated_at timestamp back by 48 hours
		newProfile := mobileconfigForTest("N5", "I5")
		testProfiles = append(testProfiles, newProfile)
		hostProfsByIdent["I5"] = &fleet.HostMacOSProfile{Identifier: "I5", DisplayName: "N5", InstallDate: time.Now()}
		s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		setProfileUploadedAt(t, time.Now().Add(-48*time.Hour), "I1", "I2", mobileconfig.FleetdConfigPayloadIdentifier, "I3", "I4", "I5")

		// trigger a profile sync and confirm that the install profile command for I3 was sent and
		// simulate a device error
		s.awaitTriggerProfileSchedule(t)
		installs, removes := checkNextPayloads(t, mdmDevice, true)
		s.signedProfilesMatch([][]byte{newProfile}, installs)
		require.Empty(t, removes)
		expectedProfileStatuses["I5"] = fleet.MDMDeliveryPending
		checkProfilesStatus(t)
		expectedRetryCounts["I5"] = 1
		checkRetryCounts(t)

		// trigger a profile sync and confirm that the install profile command for I5 was sent and
		// simulate a device ack
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, false)
		s.signedProfilesMatch([][]byte{newProfile}, installs)
		require.Empty(t, removes)
		expectedProfileStatuses["I5"] = fleet.MDMDeliveryVerifying
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// report osquery results with I5 found and confirm that the I5 marked as verified
		reportHostProfs(t, "I2", mobileconfig.FleetdConfigPayloadIdentifier, "I5")
		expectedProfileStatuses["I5"] = fleet.MDMDeliveryVerified
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile command for I5 was not resent
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, false)
		require.Empty(t, installs)
		require.Empty(t, removes)

		// report osquery results again, this time I5 is missing and confirm that the I5 marked as
		// failed (max retries exceeded)
		reportHostProfs(t, "I2", mobileconfig.FleetdConfigPayloadIdentifier)
		expectedProfileStatuses["I5"] = fleet.MDMDeliveryFailed
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile command for I5 was not resent
		s.awaitTriggerProfileSchedule(t)
		installs, removes = checkNextPayloads(t, mdmDevice, false)
		require.Empty(t, installs)
		require.Empty(t, removes)
	})
}

type profileData struct {
	Status string
	LocURI string
	Data   string
}

// reportWindowsOSQueryProfiles simulates a Windows host reporting the status of MDM profiles from OSQuery results.
func (s *integrationMDMTestSuite) reportWindowsOSQueryProfiles(ctx context.Context, t *testing.T, host *fleet.Host, hostProfileReports map[string][]profileData) {
	var responseOps []*fleet.SyncMLCmd
	for profileName, report := range hostProfileReports {
		for _, p := range report {
			ref := microsoft_mdm.HashLocURI(profileName, p.LocURI)
			responseOps = append(responseOps, &fleet.SyncMLCmd{
				XMLName: xml.Name{Local: fleet.CmdStatus},
				CmdID:   fleet.CmdID{Value: uuid.NewString()},
				CmdRef:  &ref,
				Data:    ptr.String(p.Status),
			})

			// the protocol can respond with only a `Status`
			// command if the status failed
			if p.Status != "200" || p.Data != "" {
				responseOps = append(responseOps, &fleet.SyncMLCmd{
					XMLName: xml.Name{Local: fleet.CmdResults},
					CmdID:   fleet.CmdID{Value: uuid.NewString()},
					CmdRef:  &ref,
					Items: []fleet.CmdItem{
						{Target: ptr.String(p.LocURI), Data: &fleet.RawXmlData{Content: p.Data}},
					},
				})
			}
		}
	}

	msg, err := createSyncMLMessage("2", "2", "foo", "bar", responseOps)
	require.NoError(t, err)
	out, err := xml.Marshal(msg)
	require.NoError(t, err)
	require.NoError(t, microsoft_mdm.VerifyHostMDMProfiles(ctx, s.logger, s.ds, host, out))
}

func (s *integrationMDMTestSuite) TestWindowsProfileRetries() {
	t := s.T()
	ctx := context.Background()

	testProfiles := []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "L1", Data: "D1"}})},
		{Name: "N2", Contents: syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "L2", Data: "D2"}, {Verb: "Add", LocURI: "L3", Data: "D3"}})},
	}

	h, mdmDevice := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	expectedProfileStatuses := map[string]fleet.MDMDeliveryStatus{
		"N1": fleet.MDMDeliveryVerifying,
		"N2": fleet.MDMDeliveryVerifying,
	}
	checkProfilesStatus := func(t *testing.T) {
		storedProfs, err := s.ds.GetHostMDMWindowsProfiles(ctx, h.UUID)
		require.NoError(t, err)
		require.Len(t, storedProfs, len(expectedProfileStatuses))
		for _, p := range storedProfs {
			want, ok := expectedProfileStatuses[p.Name]
			require.True(t, ok, "unexpected profile: %s", p.Name)
			require.Equal(t, want, *p.Status, "expected status %s but got %s for profile: %s", want, *p.Status, p.Name)
		}
	}

	expectedRetryCounts := map[string]uint{
		"N1": 0,
		"N2": 0,
	}
	checkRetryCounts := func(t *testing.T) {
		counts, err := s.ds.GetHostMDMProfilesRetryCounts(ctx, h)
		require.NoError(t, err)
		require.Len(t, counts, len(expectedRetryCounts))
		for _, c := range counts {
			want, ok := expectedRetryCounts[c.ProfileName]
			require.True(t, ok, "unexpected profile: %s", c.ProfileName)
			require.Equal(t, want, c.Retries, "expected retry count %d but got %d for profile: %s", want, c.Retries, c.ProfileName)
		}
	}

	hostProfileReports := map[string][]profileData{
		"N1": {{"200", "L1", "D1"}},
		"N2": {{"200", "L2", "D2"}, {"200", "L3", "D3"}},
	}
	reportHostProfs := func(profileNames ...string) {
		selectedReports := make(map[string][]profileData)
		for _, name := range profileNames {
			if reports, exists := hostProfileReports[name]; exists {
				selectedReports[name] = reports
			}
		}
		s.reportWindowsOSQueryProfiles(ctx, t, h, selectedReports)
	}

	verifyCommands := func(wantProfileInstalls int, status string) {
		s.awaitTriggerProfileSchedule(t)
		cmds, err := mdmDevice.StartManagementSession()
		require.NoError(t, err)
		// profile installs + 2 protocol commands acks
		require.Len(t, cmds, wantProfileInstalls+2)
		msgID, err := mdmDevice.GetCurrentMsgID()
		require.NoError(t, err)
		atomicCmds := 0
		for _, c := range cmds {
			if c.Verb == "Atomic" {
				atomicCmds++
			}
			mdmDevice.AppendResponse(fleet.SyncMLCmd{
				XMLName: xml.Name{Local: fleet.CmdStatus},
				MsgRef:  &msgID,
				CmdRef:  ptr.String(c.Cmd.CmdID.Value),
				Cmd:     ptr.String(c.Verb),
				Data:    ptr.String(status),
				Items:   nil,
				CmdID:   fleet.CmdID{Value: uuid.NewString()},
			})
		}
		require.Equal(t, wantProfileInstalls, atomicCmds)
		cmds, err = mdmDevice.SendResponse()
		require.NoError(t, err)
		// the ack of the message should be the only returned command
		require.Len(t, cmds, 1)
	}

	t.Run("retry after verifying", func(t *testing.T) {
		// upload test profiles then simulate expired grace period by setting updated_at timestamp of profiles back by 48 hours
		s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		// profiles to install + 2 boilerplate <Status>
		verifyCommands(len(testProfiles), syncml.CmdStatusOK)
		checkProfilesStatus(t) // all profiles verifying
		checkRetryCounts(t)    // no retries yet

		// report osquery results with N2 missing and confirm N2 marked
		// as verifying and other profiles are marked as verified
		reportHostProfs("N1")
		expectedProfileStatuses["N2"] = fleet.MDMDeliveryPending
		expectedProfileStatuses["N1"] = fleet.MDMDeliveryVerified
		checkProfilesStatus(t)
		expectedRetryCounts["N2"] = 1
		checkRetryCounts(t)

		// report osquery results with N2 present and confirm that all profiles are verified
		verifyCommands(1, syncml.CmdStatusOK)
		reportHostProfs("N1", "N2")
		expectedProfileStatuses["N2"] = fleet.MDMDeliveryVerified
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that no profiles were sent
		verifyCommands(0, syncml.CmdStatusOK)
	})

	t.Run("retry after verification", func(t *testing.T) {
		// report osquery results with N1 missing and confirm that the N1 marked as pending (initial retry)
		reportHostProfs("N2")
		expectedProfileStatuses["N1"] = fleet.MDMDeliveryPending
		checkProfilesStatus(t)
		expectedRetryCounts["N1"] = 1
		checkRetryCounts(t)

		// trigger a profile sync and confirm that the install profile command for N1 was resent
		verifyCommands(1, syncml.CmdStatusOK)

		// report osquery results with N1 missing again and confirm that the N1 marked as failed (max retries exceeded)
		reportHostProfs("N2")
		expectedProfileStatuses["N1"] = fleet.MDMDeliveryFailed
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile command for N1 was not resent
		verifyCommands(0, syncml.CmdStatusOK)
	})

	t.Run("retry after device error", func(t *testing.T) {
		// add another profile
		newProfile := syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "L3", Data: "D3"}})
		testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{
			Name:     "N3",
			Contents: newProfile,
		})
		s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		// trigger a profile sync and confirm that the install profile command for N3 was sent and
		// simulate a device error
		verifyCommands(1, syncml.CmdStatusAtomicFailed)
		expectedProfileStatuses["N3"] = fleet.MDMDeliveryPending
		checkProfilesStatus(t)
		expectedRetryCounts["N3"] = 1
		checkRetryCounts(t)

		// trigger a profile sync and confirm that the install profile command for N3 was sent and
		// simulate a device ack
		verifyCommands(1, syncml.CmdStatusOK)
		expectedProfileStatuses["N3"] = fleet.MDMDeliveryVerifying
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// report osquery results with N3 missing and confirm that the N3 marked as failed (max
		// retries exceeded)
		reportHostProfs("N2")
		expectedProfileStatuses["N3"] = fleet.MDMDeliveryFailed
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile command for N3 was not resent
		verifyCommands(0, syncml.CmdStatusOK)
	})

	t.Run("repeated device error", func(t *testing.T) {
		// add another profile
		testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{
			Name:     "N4",
			Contents: syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "L4", Data: "D4"}}),
		})
		s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		// trigger a profile sync and confirm that the install profile command for N4 was sent and
		// simulate a device error
		verifyCommands(1, syncml.CmdStatusAtomicFailed)
		expectedProfileStatuses["N4"] = fleet.MDMDeliveryPending
		checkProfilesStatus(t)
		expectedRetryCounts["N4"] = 1
		checkRetryCounts(t)

		// trigger a profile sync and confirm that the install profile
		// command for N4 was sent and simulate a second device error
		verifyCommands(1, syncml.CmdStatusAtomicFailed)
		expectedProfileStatuses["N4"] = fleet.MDMDeliveryFailed
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile
		// command for N4 was not resent
		verifyCommands(0, syncml.CmdStatusOK)
	})

	t.Run("retry count does not reset", func(t *testing.T) {
		// add another profile
		testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{
			Name:     "N5",
			Contents: syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "L5", Data: "D5"}}),
		})
		// hostProfsByIdent["N5"] = &fleet.HostMacOSProfile{Identifier: "N5", DisplayName: "N5", InstallDate: time.Now()}
		s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		// trigger a profile sync and confirm that the install profile
		// command for N5 was sent and simulate a device error
		verifyCommands(1, syncml.CmdStatusAtomicFailed)
		expectedProfileStatuses["N5"] = fleet.MDMDeliveryPending
		checkProfilesStatus(t)
		expectedRetryCounts["N5"] = 1
		checkRetryCounts(t)

		// trigger a profile sync and confirm that the install profile
		// command for N5 was sent and simulate a device ack
		verifyCommands(1, syncml.CmdStatusOK)
		expectedProfileStatuses["N5"] = fleet.MDMDeliveryVerifying
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// report osquery results with N5 found and confirm that the N5 marked as verified
		hostProfileReports["N5"] = []profileData{{"200", "L5", "D5"}}
		reportHostProfs("N2", "N5")
		expectedProfileStatuses["N5"] = fleet.MDMDeliveryVerified
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile command for N5 was not resent
		verifyCommands(0, syncml.CmdStatusOK)

		// report osquery results again, this time N5 is missing and confirm that the N5 marked as
		// failed (max retries exceeded)
		reportHostProfs("N2")
		expectedProfileStatuses["N5"] = fleet.MDMDeliveryFailed
		checkProfilesStatus(t)
		checkRetryCounts(t) // unchanged

		// trigger a profile sync and confirm that the install profile command for N5 was not resent
		verifyCommands(0, syncml.CmdStatusOK)
	})
}

// TestWindowsProfileResend verifies that a Windows profile is resent when its contents have been modified.
func (s *integrationMDMTestSuite) TestWindowsProfileResend() {
	t := s.T()
	ctx := context.Background()

	testProfiles := []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "L1", Data: "D1"}})},
		{Name: "N2", Contents: syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "L2", Data: "D2"}, {Verb: "Replace", LocURI: "L3", Data: "D3"}})},
	}

	h, mdmDevice := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	expectedProfileStatuses := map[string]fleet.MDMDeliveryStatus{
		"N1": fleet.MDMDeliveryVerifying,
		"N2": fleet.MDMDeliveryVerifying,
	}
	checkProfilesStatus := func(t *testing.T) {
		storedProfs, err := s.ds.GetHostMDMWindowsProfiles(ctx, h.UUID)
		require.NoError(t, err)
		require.Len(t, storedProfs, len(expectedProfileStatuses))
		for _, p := range storedProfs {
			want, ok := expectedProfileStatuses[p.Name]
			require.True(t, ok, "unexpected profile: %s", p.Name)
			require.Equal(t, want, *p.Status, "expected status %s but got %s for profile: %s", want, *p.Status, p.Name)
		}
	}

	hostProfileReports := map[string][]profileData{
		"N1": {{"200", "L1", "D1"}},
		"N2": {{"200", "L2", "D2"}, {"200", "L3", "D3"}},
	}
	reportHostProfs := func(profileNames ...string) {
		selectedReports := make(map[string][]profileData)
		for _, name := range profileNames {
			if reports, exists := hostProfileReports[name]; exists {
				selectedReports[name] = reports
			}
		}
		s.reportWindowsOSQueryProfiles(ctx, t, h, selectedReports)
	}

	verifyCommands := func(wantProfileInstalls int, status string) {
		s.awaitTriggerProfileSchedule(t)
		cmds, err := mdmDevice.StartManagementSession()
		require.NoError(t, err)
		// profile installs + 2 protocol commands acks
		require.Len(t, cmds, wantProfileInstalls+2)
		msgID, err := mdmDevice.GetCurrentMsgID()
		require.NoError(t, err)
		atomicCmds := 0
		for _, c := range cmds {
			if c.Verb == "Atomic" {
				atomicCmds++
			}
			mdmDevice.AppendResponse(fleet.SyncMLCmd{
				XMLName: xml.Name{Local: fleet.CmdStatus},
				MsgRef:  &msgID,
				CmdRef:  ptr.String(c.Cmd.CmdID.Value),
				Cmd:     ptr.String(c.Verb),
				Data:    ptr.String(status),
				Items:   nil,
				CmdID:   fleet.CmdID{Value: uuid.NewString()},
			})
		}
		require.Equal(t, wantProfileInstalls, atomicCmds)
		cmds, err = mdmDevice.SendResponse()
		require.NoError(t, err)
		// the ack of the message should be the only returned command
		require.Len(t, cmds, 1)
	}

	t.Run("do not resend if nothing changed", func(t *testing.T) {
		t.Cleanup(func() {
			// Clear the profiles
			s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{}},
				http.StatusNoContent)
		})

		s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		// profiles to install + 2 boilerplate <Status>
		verifyCommands(len(testProfiles), syncml.CmdStatusOK)
		expectedProfileStatuses["N1"] = fleet.MDMDeliveryVerifying
		expectedProfileStatuses["N2"] = fleet.MDMDeliveryVerifying
		checkProfilesStatus(t) // all profiles verifying

		// report osquery results and confirm that all profiles are verified
		reportHostProfs("N1", "N2")
		expectedProfileStatuses["N1"] = fleet.MDMDeliveryVerified
		expectedProfileStatuses["N2"] = fleet.MDMDeliveryVerified
		checkProfilesStatus(t)

		// trigger a profile sync and confirm that no profiles were sent
		verifyCommands(0, syncml.CmdStatusOK)

		// Upload the same profiles again
		s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		// trigger a profile sync and confirm that no profiles were sent
		verifyCommands(0, syncml.CmdStatusOK)
	})

	t.Run("resend if contents changed", func(t *testing.T) {
		t.Cleanup(func() {
			// Clear the profiles
			s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{}},
				http.StatusNoContent)
		})

		s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
		// profiles to install + 2 boilerplate <Status>
		verifyCommands(len(testProfiles), syncml.CmdStatusOK)
		expectedProfileStatuses["N1"] = fleet.MDMDeliveryVerifying
		expectedProfileStatuses["N2"] = fleet.MDMDeliveryVerifying
		checkProfilesStatus(t) // all profiles verifying

		// report osquery results and confirm that all profiles are verified
		reportHostProfs("N1", "N2")
		expectedProfileStatuses["N1"] = fleet.MDMDeliveryVerified
		expectedProfileStatuses["N2"] = fleet.MDMDeliveryVerified
		checkProfilesStatus(t)

		// trigger a profile sync and confirm that no profiles were sent
		verifyCommands(0, syncml.CmdStatusOK)

		// Change one profile and upload
		copiedTestProfiles := make([]fleet.MDMProfileBatchPayload, len(testProfiles))
		copy(copiedTestProfiles, testProfiles)
		copiedTestProfiles[0].Contents = syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "L1", Data: "D1-Modified"}})
		s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: copiedTestProfiles}, http.StatusNoContent)
		// Confirm that one profile was sent and its status
		verifyCommands(1, syncml.CmdStatusOK)
		expectedProfileStatuses["N1"] = fleet.MDMDeliveryVerifying
		expectedProfileStatuses["N2"] = fleet.MDMDeliveryVerified
		checkProfilesStatus(t) // all profiles verifying
	})
}

func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
	ctx := context.Background()
	t := s.T()

	// before we switch to a gitops token, ensure ABM is setup
	s.enableABM(t.Name())

	// Use a gitops user for all Puppet actions
	u := &fleet.User{
		Name:       "GitOps",
		Email:      "gitops-TestPuppetMatchPreassignProfiles@example.com",
		GlobalRole: ptr.String(fleet.RoleGitOps),
	}
	require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
	_, err := s.ds.NewUser(context.Background(), u)
	require.NoError(t, err)
	s.setTokenForTest(t, "gitops-TestPuppetMatchPreassignProfiles@example.com", test.GoodPassword)

	runWithAdminToken := func(cb func()) {
		s.token = s.getTestAdminToken()
		cb()
		s.token = s.getCachedUserToken("gitops-TestPuppetMatchPreassignProfiles@example.com", test.GoodPassword)
	}

	// create a host enrolled in fleet
	mdmHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)

	// create a host that's not enrolled into MDM
	nonMDMHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
		OsqueryHostID: ptr.String("not-mdm-enrolled"),
		NodeKey:       ptr.String("not-mdm-enrolled"),
		UUID:          uuid.New().String(),
		Hostname:      fmt.Sprintf("%sfoo.local.not.enrolled", t.Name()),
		Platform:      "darwin",
	})
	require.NoError(t, err)

	// create a setup assistant for no team, for this we need to:
	// 1. mock the ABM API, as it gets called to set the profile
	// 2. run the DEP schedule, as this registers the default profile
	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
	}))
	s.runDEPSchedule()
	noTeamProf := `{"x": 1}`
	var globalAsstResp createMDMAppleSetupAssistantResponse
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "no-team",
		EnrollmentProfile: json.RawMessage(noTeamProf),
	}, http.StatusOK, &globalAsstResp)

	// set the global Enable Release Device manually setting to true,
	// will be inherited by teams created via preassign/match.
	s.Do("PATCH", "/api/latest/fleet/setup_experience",
		json.RawMessage(jsonMustMarshal(t, map[string]any{"enable_release_device_manually": true})),
		http.StatusNoContent)

	s.runWorker()

	// preassign an empty profile, fails
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "empty", HostUUID: nonMDMHost.UUID, Profile: nil}}, http.StatusUnprocessableEntity)

	// preassign a valid profile to the MDM host
	prof1 := mobileconfigForTest("n1", "i1")
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm1", HostUUID: mdmHost.UUID, Profile: prof1}}, http.StatusNoContent)

	// preassign another valid profile to the MDM host
	prof2 := mobileconfigForTest("n2", "i2")
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm1", HostUUID: mdmHost.UUID, Profile: prof2, Group: "g1"}}, http.StatusNoContent)

	// preassign a valid profile to the non-MDM host, still works as the host is not validated in this call
	prof3 := mobileconfigForTest("n3", "i3")
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "non-mdm", HostUUID: nonMDMHost.UUID, Profile: prof3, Group: "g2"}}, http.StatusNoContent)

	// match with an invalid external host id, succeeds as it is the same as if
	// there was no matching to do (no preassignment was done)
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "no-such-id"}, http.StatusNoContent)

	// match with the non-mdm host fails
	res := s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "non-mdm"}, http.StatusBadRequest)
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "host is not enrolled in Fleet MDM")

	// match with the mdm host succeeds and creates a team based on the group labels
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "mdm1"}, http.StatusNoContent)

	// the host is now part of that team
	h, err := s.ds.Host(ctx, mdmHost.ID)
	require.NoError(t, err)
	require.NotNil(t, h.TeamID)
	tm1, err := s.ds.TeamLite(ctx, *h.TeamID)
	require.NoError(t, err)
	require.Equal(t, "g1", tm1.Name)
	require.True(t, tm1.Config.MDM.EnableDiskEncryption)
	require.True(t, tm1.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)

	runWithAdminToken(func() {
		// it create activities for the new team, the profiles assigned to it,
		// the host moved to it, and setup assistant
		s.lastActivityOfTypeMatches(
			fleet.ActivityTypeCreatedTeam{}.ActivityName(),
			fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm1.ID, tm1.Name),
			0)
		s.lastActivityOfTypeMatches(
			fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
			fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm1.ID, tm1.Name),
			0)
		s.lastActivityOfTypeMatches(
			fleet.ActivityTypeTransferredHostsToTeam{}.ActivityName(),
			fmt.Sprintf(`{"team_id": %d, "team_name": %q, "host_ids": [%d], "host_display_names": [%q]}`,
				tm1.ID, tm1.Name, h.ID, h.DisplayName()),
			0)
		s.lastActivityOfTypeMatches(
			fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
			fmt.Sprintf(`{"team_id": %d, "name": %q, "team_name": %q}`,
				tm1.ID, globalAsstResp.Name, tm1.Name),
			0)
	})

	// and the team has the expected profiles (prof1 and prof2)
	profs, err := s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID)
	require.NoError(t, err)
	require.Len(t, profs, 2)
	// order is guaranteed by profile name
	require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
	require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
	// setup assistant settings are copyied from "no team"
	teamAsst, err := s.ds.GetMDMAppleSetupAssistant(ctx, &tm1.ID)
	require.NoError(t, err)
	require.Equal(t, globalAsstResp.Name, teamAsst.Name)
	require.JSONEq(t, string(globalAsstResp.Profile), string(teamAsst.Profile))

	// trigger the schedule so profiles are set in their state
	s.awaitTriggerProfileSchedule(t)
	s.runWorker()

	// the mdm host has the same profiles (i1, i2, plus fleetd config and disk encryption)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		mdmHost: {
			{Identifier: "i1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "i2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	// create a team and set profiles to it (note that it doesn't have disk encryption enabled)
	tm2, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:    "g1 - g4",
		Secrets: []*fleet.EnrollSecret{{Secret: "tm2secret"}},
	})
	require.NoError(t, err)
	prof4 := mobileconfigForTest("n4", "i4")
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
		prof1, prof4,
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm2.ID))
	// tm2 has disk encryption and release device manually disabled
	require.False(t, tm2.Config.MDM.EnableDiskEncryption)
	require.False(t, tm2.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)

	// create another team with a superset of profiles
	tm3, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:    "team3_" + t.Name(),
		Secrets: []*fleet.EnrollSecret{{Secret: "tm3secret"}},
	})
	require.NoError(t, err)
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
		prof1, prof2, prof4,
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm3.ID))

	// and yet another team with the same profiles as tm3
	tm4, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:    "team4_" + t.Name(),
		Secrets: []*fleet.EnrollSecret{{Secret: "tm4secret"}},
	})
	require.NoError(t, err)
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
		prof1, prof2, prof4,
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm4.ID))

	// preassign the MDM host to prof1 and prof4, should match existing team tm2
	//
	// additionally, use external host identifiers with different
	// suffixes to simulate real world distributed scenarios where more
	// than one puppet server might be running at the time.
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "6f36ab2c-1a40-429b-9c9d-07c9029f4aa8-puppetcompiler06.test.example.com", HostUUID: mdmHost.UUID, Profile: prof1, Group: "g1"}}, http.StatusNoContent)
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "6f36ab2c-1a40-429b-9c9d-07c9029f4aa8-puppetcompiler01.test.example.com", HostUUID: mdmHost.UUID, Profile: prof4, Group: "g4"}}, http.StatusNoContent)

	// match with the mdm host succeeds and assigns it to tm2
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "6f36ab2c-1a40-429b-9c9d-07c9029f4aa8-puppetcompiler03.test.example.com"}, http.StatusNoContent)

	// the host is now part of that team
	h, err = s.ds.Host(ctx, mdmHost.ID)
	require.NoError(t, err)
	require.NotNil(t, h.TeamID)
	require.Equal(t, tm2.ID, *h.TeamID)

	// tmLite2 still has disk encryption and release device manually disabled
	tmLite2, err := s.ds.TeamLite(ctx, *h.TeamID)
	require.NoError(t, err)
	require.False(t, tmLite2.Config.MDM.EnableDiskEncryption)
	require.False(t, tmLite2.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)

	// the host's profiles are:
	// - the same as the team's and are pending (prof1 + prof4)
	// - prof2 + old filevault are pending removal
	// - fleetd config being reinstalled (for new enroll secret)
	s.awaitTriggerProfileSchedule(t)

	// useful for debugging
	// mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
	// 	mysql.DumpTable(t, q, "host_mdm_apple_profiles")
	// 	return nil
	// })
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		mdmHost: {
			{Identifier: "i1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "i4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			// Profiles from previous team being deleted
			{Identifier: "i2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{
				Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, OperationType: fleet.MDMOperationTypeRemove,
				Status: &fleet.MDMDeliveryPending,
			},
		},
	})

	// create a new mdm host enrolled in fleet
	mdmHost2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)

	// make it part of team 2
	s.Do("POST", "/api/v1/fleet/hosts/transfer",
		addHostsToTeamRequest{TeamID: &tmLite2.ID, HostIDs: []uint{mdmHost2.ID}}, http.StatusOK)

	// simulate having its profiles installed
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		res, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ?`, fleet.OSSettingsVerifying, mdmHost2.UUID)
		n, _ := res.RowsAffected()
		require.Equal(t, 4, int(n))
		return err
	})

	// preassign the MDM host using "g1" and "g4", should match existing
	// team tmLite2, and nothing be done since the host is already in tmLite2
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm2", HostUUID: mdmHost2.UUID, Profile: prof1, Group: "g1"}}, http.StatusNoContent)
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm2", HostUUID: mdmHost2.UUID, Profile: prof4, Group: "g4"}}, http.StatusNoContent)
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "mdm2"}, http.StatusNoContent)

	// the host is still part of tmLite2
	h, err = s.ds.Host(ctx, mdmHost2.ID)
	require.NoError(t, err)
	require.NotNil(t, h.TeamID)
	require.Equal(t, tmLite2.ID, *h.TeamID)

	// and its profiles have been left untouched
	s.awaitTriggerProfileSchedule(t)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		mdmHost2: {
			{Identifier: "i1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "i4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
}

// while s.TestPuppetMatchPreassignProfiles focuses on many edge cases/extra
// checks around profile assignment, this test is mainly focused on
// simulating a few puppet runs in scenarios we want to support, and ensuring that:
//
// - different hosts end up in the right teams
// - teams get edited as expected
// - commands to add/remove profiles are issued adequately
func (s *integrationMDMTestSuite) TestPuppetRun() {
	t := s.T()
	ctx := context.Background()

	// define a few profiles
	prof1, prof2, prof3, prof4 := mobileconfigForTest("n1", "i1"),
		mobileconfigForTest("n2", "i2"),
		mobileconfigForTest("n3", "i3"),
		mobileconfigForTest("n4", "i4")

	// create three hosts
	host1, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	host2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	host3, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	s.runWorker()

	// Set up a mock Apple DEP API
	s.enableABM(t.Name())
	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		encoder := json.NewEncoder(w)
		switch r.URL.Path {
		case "/session":
			_, _ = w.Write([]byte(`{"auth_session_token": "session123"}`))
		case "/account":
			_, _ = w.Write([]byte(fmt.Sprintf(`{"admin_id": "admin123", "org_name": "%s"}`, "foo")))
		case "/profile":
			w.WriteHeader(http.StatusOK)
			require.NoError(t, encoder.Encode(godep.ProfileResponse{ProfileUUID: "profile123"}))
		}
	}))

	// Use a gitops user for all Puppet actions
	u := &fleet.User{
		Name:       "GitOps",
		Email:      "gitops-TestPuppetRun@example.com",
		GlobalRole: ptr.String(fleet.RoleGitOps),
	}
	require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
	_, err := s.ds.NewUser(context.Background(), u)
	require.NoError(t, err)
	s.setTokenForTest(t, "gitops-TestPuppetRun@example.com", test.GoodPassword)

	// preassignAndMatch simulates the puppet module doing all the
	// preassign/match calls for a given set of profiles.
	preassignAndMatch := func(profs []fleet.MDMApplePreassignProfilePayload) {
		require.NotEmpty(t, profs)
		for _, prof := range profs {
			s.Do(
				"POST",
				"/api/latest/fleet/mdm/apple/profiles/preassign",
				preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: prof},
				http.StatusNoContent,
			)
		}
		s.Do(
			"POST",
			"/api/latest/fleet/mdm/apple/profiles/match",
			matchMDMApplePreassignmentRequest{ExternalHostIdentifier: profs[0].ExternalHostIdentifier},
			http.StatusNoContent,
		)
	}

	// node default {
	//   fleetdm::profile { 'n1':
	//     template => template('n1.mobileconfig.erb'),
	//     group    => 'base',
	//   }
	//
	//   fleetdm::profile { 'n2':
	//     template => template('n2.mobileconfig.erb'),
	//     group    => 'workstations',
	//   }
	//
	//   fleetdm::profile { 'n3':
	//     template => template('n3.mobileconfig.erb'),
	//     group    => 'workstations',
	//   }
	//
	//   if $facts['system_profiler']['hardware_uuid'] == 'host_2_uuid' {
	//       fleetdm::profile { 'n4':
	//         template => template('fleetdm/n4.mobileconfig.erb'),
	//         group    => 'kiosks',
	//       }
	//   }
	puppetRun := func(host *fleet.Host) {
		payload := []fleet.MDMApplePreassignProfilePayload{
			{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof1,
				Group:                  "base",
			},
			{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof2,
				Group:                  "workstations",
			},
			{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof3,
				Group:                  "workstations",
			},
		}

		if host.UUID == host2.UUID {
			payload = append(payload, fleet.MDMApplePreassignProfilePayload{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof4,
				Group:                  "kiosks",
			})
		}

		preassignAndMatch(payload)
	}

	// host1 checks in
	puppetRun(host1)

	// the host now belongs to a team
	h1, err := s.ds.Host(ctx, host1.ID)
	require.NoError(t, err)
	require.NotNil(t, h1.TeamID)

	// the team has the right name
	tm1, err := s.ds.TeamWithExtras(ctx, *h1.TeamID)
	require.NoError(t, err)
	require.Equal(t, "base - workstations", tm1.Name)
	// and the right profiles
	profs, err := s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID)
	require.NoError(t, err)
	require.Len(t, profs, 3)
	require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
	require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
	require.Equal(t, prof3, []byte(profs[2].Mobileconfig))
	require.True(t, tm1.Config.MDM.EnableDiskEncryption)

	// host2 checks in
	puppetRun(host2)
	// a new team is created
	h2, err := s.ds.Host(ctx, host2.ID)
	require.NoError(t, err)
	require.NotNil(t, h2.TeamID)

	// the team has the right name
	tm2, err := s.ds.TeamWithExtras(ctx, *h2.TeamID)
	require.NoError(t, err)
	require.Equal(t, "base - kiosks - workstations", tm2.Name)
	// and the right profiles
	profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm2.ID)
	require.NoError(t, err)
	require.Len(t, profs, 4)
	require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
	require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
	require.Equal(t, prof3, []byte(profs[2].Mobileconfig))
	require.Equal(t, prof4, []byte(profs[3].Mobileconfig))
	require.True(t, tm2.Config.MDM.EnableDiskEncryption)

	// host3 checks in
	puppetRun(host3)
	// it belongs to the same team as host1
	h3, err := s.ds.Host(ctx, host3.ID)
	require.NoError(t, err)
	require.Equal(t, h1.TeamID, h3.TeamID)

	// prof2 is edited
	oldProf2 := prof2
	prof2 = mobileconfigForTest("n2", "i2-v2")
	// host3 checks in again
	puppetRun(host3)
	// still belongs to the same team
	h3, err = s.ds.Host(ctx, host3.ID)
	require.NoError(t, err)
	require.Equal(t, tm1.ID, *h3.TeamID)

	// but the team has prof2 updated
	profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID)
	require.NoError(t, err)
	require.Len(t, profs, 3)
	require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
	require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
	require.Equal(t, prof3, []byte(profs[2].Mobileconfig))
	require.NotEqual(t, oldProf2, []byte(profs[1].Mobileconfig))
	require.True(t, tm1.Config.MDM.EnableDiskEncryption)

	// host2 checks in, still belongs to the same team
	puppetRun(host2)
	h2, err = s.ds.Host(ctx, host2.ID)
	require.NoError(t, err)
	require.Equal(t, tm2.ID, *h2.TeamID)

	// but the team has prof2 updated as well
	profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm2.ID)
	require.NoError(t, err)
	require.Len(t, profs, 4)
	require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
	require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
	require.Equal(t, prof3, []byte(profs[2].Mobileconfig))
	require.Equal(t, prof4, []byte(profs[3].Mobileconfig))
	require.NotEqual(t, oldProf2, []byte(profs[1].Mobileconfig))
	require.True(t, tm1.Config.MDM.EnableDiskEncryption)

	// the puppet manifest is changed, and prof3 is removed
	// node default {
	//   fleetdm::profile { 'n1':
	//     template => template('n1.mobileconfig.erb'),
	//     group    => 'base',
	//   }
	//
	//   fleetdm::profile { 'n2':
	//     template => template('n2.mobileconfig.erb'),
	//     group    => 'workstations',
	//   }
	//
	//   if $facts['system_profiler']['hardware_uuid'] == 'host_2_uuid' {
	//       fleetdm::profile { 'n4':
	//         template => template('fleetdm/n4.mobileconfig.erb'),
	//         group    => 'kiosks',
	//       }
	//   }
	puppetRun = func(host *fleet.Host) {
		payload := []fleet.MDMApplePreassignProfilePayload{
			{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof1,
				Group:                  "base",
			},
			{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof2,
				Group:                  "workstations",
			},
		}

		if host.UUID == host2.UUID {
			payload = append(payload, fleet.MDMApplePreassignProfilePayload{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof4,
				Group:                  "kiosks",
			})
		}

		preassignAndMatch(payload)
	}

	// host1 checks in again
	puppetRun(host1)
	// still belongs to the same team
	h1, err = s.ds.Host(ctx, host1.ID)
	require.NoError(t, err)
	require.Equal(t, tm1.ID, *h1.TeamID)

	// but the team doesn't have prof3 anymore
	profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID)
	require.NoError(t, err)
	require.Len(t, profs, 2)
	require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
	require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
	require.True(t, tm1.Config.MDM.EnableDiskEncryption)

	// same for host2
	puppetRun(host2)
	h2, err = s.ds.Host(ctx, host2.ID)
	require.NoError(t, err)
	require.Equal(t, tm2.ID, *h2.TeamID)
	profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm2.ID)
	require.NoError(t, err)
	require.Len(t, profs, 3)
	require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
	require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
	require.Equal(t, prof4, []byte(profs[2].Mobileconfig))
	require.True(t, tm1.Config.MDM.EnableDiskEncryption)

	// The puppet manifest is drastically updated, this time to use exclusions on host3:
	//
	// node default {
	//   fleetdm::profile { 'n1':
	//     template => template('n1.mobileconfig.erb'),
	//     group    => 'base',
	//   }
	//
	//   fleetdm::profile { 'n2':
	//     template => template('n2.mobileconfig.erb'),
	//     group    => 'workstations',
	//   }
	//
	//   if $facts['system_profiler']['hardware_uuid'] == 'host_3_uuid' {
	//       fleetdm::profile { 'n3':
	//         template => template('fleetdm/n3.mobileconfig.erb'),
	//         group    => 'no-nudge',
	//       }
	//   } else {
	//       fleetdm::profile { 'n3':
	//         ensure => absent,
	//         template => template('fleetdm/n3.mobileconfig.erb'),
	//         group    => 'workstations',
	//       }
	//   }
	// }
	puppetRun = func(host *fleet.Host) {
		manifest := []fleet.MDMApplePreassignProfilePayload{
			{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof1,
				Group:                  "base",
			},
			{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof2,
				Group:                  "workstations",
			},
		}

		if host.UUID == host3.UUID {
			manifest = append(manifest, fleet.MDMApplePreassignProfilePayload{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof3,
				Group:                  "no-nudge",
				Exclude:                true,
			})
		} else {
			manifest = append(manifest, fleet.MDMApplePreassignProfilePayload{
				ExternalHostIdentifier: host.Hostname,
				HostUUID:               host.UUID,
				Profile:                prof3,
				Group:                  "workstations",
			})
		}

		preassignAndMatch(manifest)
	}

	// host1 checks in
	puppetRun(host1)

	// the host belongs to the same team
	h1, err = s.ds.Host(ctx, host1.ID)
	require.NoError(t, err)
	require.Equal(t, tm1.ID, *h1.TeamID)

	// the team has the right profiles
	profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID)
	require.NoError(t, err)
	require.Len(t, profs, 3)
	require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
	require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
	require.Equal(t, prof3, []byte(profs[2].Mobileconfig))
	require.True(t, tm1.Config.MDM.EnableDiskEncryption)

	// host2 checks in
	puppetRun(host2)
	// it is assigned to tm1
	h2, err = s.ds.Host(ctx, host2.ID)
	require.NoError(t, err)
	require.Equal(t, tm1.ID, *h2.TeamID)

	// host3 checks in
	puppetRun(host3)

	// it is assigned to a new team
	h3, err = s.ds.Host(ctx, host3.ID)
	require.NoError(t, err)
	require.NotNil(t, h3.TeamID)
	require.NotEqual(t, tm1.ID, *h3.TeamID)
	require.NotEqual(t, tm2.ID, *h3.TeamID)

	// a new team is created
	tm3, err := s.ds.TeamWithExtras(ctx, *h3.TeamID)
	require.NoError(t, err)
	require.Equal(t, "base - no-nudge - workstations", tm3.Name)
	// and the right profiles
	profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm3.ID)
	require.NoError(t, err)
	require.Len(t, profs, 2)
	require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
	require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
	require.True(t, tm3.Config.MDM.EnableDiskEncryption)
}

func (s *integrationMDMTestSuite) TestMDMAppleListConfigProfiles() {
	t := s.T()
	ctx := context.Background()

	testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"})
	require.NoError(t, err)

	mdmHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	s.runWorker()

	t.Run("no profiles", func(t *testing.T) {
		var listResp listMDMAppleConfigProfilesResponse
		s.DoJSON("GET", "/api/v1/fleet/mdm/apple/profiles", nil, http.StatusOK, &listResp)
		require.NotNil(t, listResp.ConfigProfiles) // expect empty slice instead of nil
		require.Len(t, listResp.ConfigProfiles, 0)

		listResp = listMDMAppleConfigProfilesResponse{}
		s.DoJSON("GET", fmt.Sprintf(`/api/v1/fleet/mdm/apple/profiles?team_id=%d`, testTeam.ID), nil, http.StatusOK, &listResp)
		require.NotNil(t, listResp.ConfigProfiles) // expect empty slice instead of nil
		require.Len(t, listResp.ConfigProfiles, 0)

		var hostProfilesResp getHostProfilesResponse
		s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d/configuration_profiles", mdmHost.ID), nil, http.StatusOK, &hostProfilesResp)
		require.NotNil(t, hostProfilesResp.Profiles) // expect empty slice instead of nil
		require.Len(t, hostProfilesResp.Profiles, 0)
		require.EqualValues(t, mdmHost.ID, hostProfilesResp.HostID)
	})

	t.Run("with profiles", func(t *testing.T) {
		p1, err := fleet.NewMDMAppleConfigProfile(mcBytesForTest("p1", "p1.identifier", "p1.uuid"), nil)
		require.NoError(t, err)
		_, err = s.ds.NewMDMAppleConfigProfile(ctx, *p1, nil)
		require.NoError(t, err)

		p2, err := fleet.NewMDMAppleConfigProfile(mcBytesForTest("p2", "p2.identifier", "p2.uuid"), &testTeam.ID)
		require.NoError(t, err)
		_, err = s.ds.NewMDMAppleConfigProfile(ctx, *p2, nil)
		require.NoError(t, err)

		var resp listMDMAppleConfigProfilesResponse
		s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesRequest{TeamID: 0}, http.StatusOK, &resp)
		require.NotNil(t, resp.ConfigProfiles)
		require.Len(t, resp.ConfigProfiles, 1)
		require.Equal(t, p1.Name, resp.ConfigProfiles[0].Name)
		require.Equal(t, p1.Identifier, resp.ConfigProfiles[0].Identifier)

		resp = listMDMAppleConfigProfilesResponse{}
		s.DoJSON("GET", fmt.Sprintf(`/api/v1/fleet/mdm/apple/profiles?team_id=%d`, testTeam.ID), nil, http.StatusOK, &resp)
		require.NotNil(t, resp.ConfigProfiles)
		require.Len(t, resp.ConfigProfiles, 1)
		require.Equal(t, p2.Name, resp.ConfigProfiles[0].Name)
		require.Equal(t, p2.Identifier, resp.ConfigProfiles[0].Identifier)

		p3, err := fleet.NewMDMAppleConfigProfile(mcBytesForTest("p3", "p3.identifier", "p3.uuid"), &testTeam.ID)
		require.NoError(t, err)
		_, err = s.ds.NewMDMAppleConfigProfile(ctx, *p3, nil)
		require.NoError(t, err)

		resp = listMDMAppleConfigProfilesResponse{}
		s.DoJSON("GET", fmt.Sprintf(`/api/v1/fleet/mdm/apple/profiles?team_id=%d`, testTeam.ID), nil, http.StatusOK, &resp)
		require.NotNil(t, resp.ConfigProfiles)
		require.Len(t, resp.ConfigProfiles, 2)
		for _, p := range resp.ConfigProfiles {
			if p.Name == p2.Name { //nolint:gocritic // ignore ifElseChain
				require.Equal(t, p2.Identifier, p.Identifier)
			} else if p.Name == p3.Name {
				require.Equal(t, p3.Identifier, p.Identifier)
			} else {
				require.Fail(t, "unexpected profile name")
			}
		}

		var hostProfilesResp getHostProfilesResponse
		s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d/configuration_profiles", mdmHost.ID), nil, http.StatusOK, &hostProfilesResp)
		require.NotNil(t, hostProfilesResp.Profiles)
		require.Len(t, hostProfilesResp.Profiles, 1)
		require.Equal(t, p1.Name, hostProfilesResp.Profiles[0].Name)
		require.Equal(t, p1.Identifier, hostProfilesResp.Profiles[0].Identifier)
		require.EqualValues(t, mdmHost.ID, hostProfilesResp.HostID)

		// add the host to a team
		err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&testTeam.ID, []uint{mdmHost.ID}))
		require.NoError(t, err)

		hostProfilesResp = getHostProfilesResponse{}
		s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d/configuration_profiles", mdmHost.ID), nil, http.StatusOK, &hostProfilesResp)
		require.NotNil(t, hostProfilesResp.Profiles)
		require.Len(t, hostProfilesResp.Profiles, 2)
		require.EqualValues(t, mdmHost.ID, hostProfilesResp.HostID)
	})
}

func (s *integrationMDMTestSuite) TestAppConfigMDMCustomSettings() {
	t := s.T()

	// set the macos custom settings fields with the deprecated Labels field
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
		  "macos_settings": {
				"custom_settings": [
					{"path": "foo", "labels": ["baz"]},
					{"path": "bar"}
				]
			}
		}
  }`), http.StatusOK, &acResp)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "foo", LabelsIncludeAll: []string{"baz"}}, {Path: "bar"}}, acResp.MDM.MacOSSettings.CustomSettings)

	// check that they are returned by a GET /config
	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "foo", LabelsIncludeAll: []string{"baz"}}, {Path: "bar"}}, acResp.MDM.MacOSSettings.CustomSettings)

	// set the windows custom settings fields with included/excluded labels
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"windows_settings": {
				"custom_settings": [
					{"path": "foo", "labels_exclude_any": ["x", "y"]},
					{"path": "bar", "labels_include_all": ["a", "b"]},
					{"path": "baz", "labels": ["c"]}
				]
			}
		}
  }`), http.StatusOK, &acResp)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "foo", LabelsIncludeAll: []string{"baz"}}, {Path: "bar"}}, acResp.MDM.MacOSSettings.CustomSettings)
	assert.Equal(t, optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "foo", LabelsExcludeAny: []string{"x", "y"}}, {Path: "bar", LabelsIncludeAll: []string{"a", "b"}}, {Path: "baz", LabelsIncludeAll: []string{"c"}}}), acResp.MDM.WindowsSettings.CustomSettings)

	// check that they are returned by a GET /config
	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "foo", LabelsIncludeAll: []string{"baz"}}, {Path: "bar"}}, acResp.MDM.MacOSSettings.CustomSettings)
	assert.Equal(t, optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "foo", LabelsExcludeAny: []string{"x", "y"}}, {Path: "bar", LabelsIncludeAll: []string{"a", "b"}}, {Path: "baz", LabelsIncludeAll: []string{"c"}}}), acResp.MDM.WindowsSettings.CustomSettings)

	// patch without specifying the windows/macos custom settings fields and an unrelated
	// field, should not remove them
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK, &acResp)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "foo", LabelsIncludeAll: []string{"baz"}}, {Path: "bar"}}, acResp.MDM.MacOSSettings.CustomSettings)
	assert.Equal(t, optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "foo", LabelsExcludeAny: []string{"x", "y"}}, {Path: "bar", LabelsIncludeAll: []string{"a", "b"}}, {Path: "baz", LabelsIncludeAll: []string{"c"}}}), acResp.MDM.WindowsSettings.CustomSettings)

	// patch with explicitly empty macos/windows custom settings fields, would remove
	// them but this is a dry-run
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"macos_settings": { "custom_settings": null },
			"windows_settings": { "custom_settings": null }
		}
  }`), http.StatusOK, &acResp, "dry_run", "true")
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "foo", LabelsIncludeAll: []string{"baz"}}, {Path: "bar"}}, acResp.MDM.MacOSSettings.CustomSettings)
	assert.Equal(t, optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "foo", LabelsExcludeAny: []string{"x", "y"}}, {Path: "bar", LabelsIncludeAll: []string{"a", "b"}}, {Path: "baz", LabelsIncludeAll: []string{"c"}}}), acResp.MDM.WindowsSettings.CustomSettings)

	// patch with explicitly empty macos custom settings fields, removes them
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"macos_settings": { "custom_settings": null },
			"windows_settings": { "custom_settings": null }
		}
  }`), http.StatusOK, &acResp)
	assert.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)
	assert.Equal(t, optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}}, acResp.MDM.WindowsSettings.CustomSettings)

	// mix of labels fields returns an error
	res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
		  "macos_settings": {
				"custom_settings": [
					{"path": "foo", "labels": ["a"], "labels_exclude_any": ["b"]}
				]
			}
		}
  }`), http.StatusUnprocessableEntity)
	msg := extractServerErrorText(res.Body)
	require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)

	res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
		  "windows_settings": {
				"custom_settings": [
					{"path": "foo", "labels_include_all": ["a"], "labels_exclude_any": ["b"]}
				]
			}
		}
  }`), http.StatusUnprocessableEntity)
	msg = extractServerErrorText(res.Body)
	require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)

	res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
		  "windows_settings": {
				"custom_settings": [
					{"path": "foo", "labels_include_any": ["a"], "labels_exclude_any": ["b"]}
				]
			}
		}
  }`), http.StatusUnprocessableEntity)
	msg = extractServerErrorText(res.Body)
	require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)

	res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
		  "windows_settings": {
				"custom_settings": [
					{"path": "foo", "labels": ["a"], "labels_include_any": ["b"]}
				]
			}
		}
  }`), http.StatusUnprocessableEntity)
	msg = extractServerErrorText(res.Body)
	require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
}

func (s *integrationMDMTestSuite) TestApplyTeamsMDMAppleProfiles() {
	t := s.T()

	// create a team through the service so it initializes the agent ops
	teamName := t.Name() + "team1"
	team := &fleet.Team{
		Name:        teamName,
		Description: "desc team1",
	}
	var createTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
	require.NotZero(t, createTeamResp.Team.ID)
	team = createTeamResp.Team

	// apply with custom macos settings
	teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{
				"custom_settings": []map[string]interface{}{
					{"path": "foo", "labels": []string{"a", "b"}},
					{"path": "bar", "labels_exclude_any": []string{"c"}},
				},
			},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)

	// retrieving the team returns the custom macos settings
	var teamResp getTeamResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.Equal(t, []fleet.MDMProfileSpec{
		{Path: "foo", LabelsIncludeAll: []string{"a", "b"}},
		{Path: "bar", LabelsExcludeAny: []string{"c"}},
	}, teamResp.Team.Config.MDM.MacOSSettings.CustomSettings)

	// apply with invalid macos settings subfield should fail
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{"foo_bar": 123},
		},
	}}}
	res := s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusBadRequest)
	errMsg := extractServerErrorText(res.Body)
	assert.Contains(t, errMsg, `unsupported key provided: "foo_bar"`)

	// apply with some good and some bad macos settings subfield should fail
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{"custom_settings": []interface{}{"A", true}},
		},
	}}}
	res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusBadRequest)
	errMsg = extractServerErrorText(res.Body)
	assert.Contains(t, errMsg, `invalid value type at 'macos_settings.custom_settings': expected array of MDMProfileSpecs but got bool`)

	// apply without custom macos settings specified and unrelated field, should
	// not replace existing settings
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(false),
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.Equal(t, []fleet.MDMProfileSpec{
		{Path: "foo", LabelsIncludeAll: []string{"a", "b"}},
		{Path: "bar", LabelsExcludeAny: []string{"c"}},
	}, teamResp.Team.Config.MDM.MacOSSettings.CustomSettings)

	// apply with explicitly empty custom macos settings would clear the existing
	// settings, but dry-run
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{"custom_settings": []map[string]interface{}{}},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, "dry_run", "true")
	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.Equal(t, []fleet.MDMProfileSpec{
		{Path: "foo", LabelsIncludeAll: []string{"a", "b"}},
		{Path: "bar", LabelsExcludeAny: []string{"c"}},
	}, teamResp.Team.Config.MDM.MacOSSettings.CustomSettings)

	// apply with explicitly empty custom macos settings clears the existing settings
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{"custom_settings": []map[string]interface{}{}},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.Equal(t, []fleet.MDMProfileSpec{}, teamResp.Team.Config.MDM.MacOSSettings.CustomSettings)

	// apply with invalid mix of labels fails
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{
				"custom_settings": []map[string]interface{}{
					{"path": "bar", "labels": []string{"x"}},
					{"path": "foo", "labels": []string{"a", "b"}, "labels_include_all": []string{"c"}},
				},
			},
		},
	}}}
	res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity)
	errMsg = extractServerErrorText(res.Body)
	assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
}

func (s *integrationMDMTestSuite) TestBatchSetMDMAppleProfiles() {
	t := s.T()
	ctx := context.Background()

	bigString := strings.Repeat("a", 1024*1024+1)

	// create a new team
	tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"})
	require.NoError(t, err)

	// apply an empty set to no-team
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: nil}, http.StatusNoContent)
	s.lastActivityMatches(
		fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
		`{"team_id": null, "team_name": null}`,
		0,
	)

	// apply to both team id and name
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: nil},
		http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID), "team_name", tm.Name)

	// invalid team name
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: nil},
		http.StatusNotFound, "team_name", uuid.New().String())

	// Profile is too big
	resp := s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{[]byte(bigString)}},
		http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(resp.Body), "maximum configuration profile file size is 1 MB")

	// duplicate profile names
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
		mobileconfigForTest("N1", "I1"),
		mobileconfigForTest("N1", "I2"),
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))

	// profiles with reserved identifiers
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		res := s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
			mobileconfigForTest("N1", "I1"),
			mobileconfigForTest(p, p),
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: payload identifier %s is not allowed", p))
	}

	// payloads with reserved types
	for p := range mobileconfig.FleetPayloadTypes() {
		if p == mobileconfig.FleetCustomSettingsPayloadType {
			// FileVault options in the custom settings payload are checked in file_vault_options_test.go
			continue
		}
		res := s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
			mobileconfigForTestWithContent("N1", "I1", "II1", p, ""),
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		switch p {
		case mobileconfig.FleetFileVaultPayloadType, mobileconfig.FleetRecoveryKeyEscrowPayloadType:
			assert.Contains(t, errMsg, mobileconfig.DiskEncryptionProfileRestrictionErrMsg)
		default:
			assert.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadType(s): %s", p))
		}
	}

	// payloads with reserved identifiers
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		res := s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
			mobileconfigForTestWithContent("N1", "I1", p, "random", ""),
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadIdentifier(s): %s", p))
	}

	// successfully apply a profile for the team
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
		mobileconfigForTest("N1", "I1"),
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID))
	s.lastActivityMatches(
		fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
		0,
	)
}

func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
	t := s.T()
	ctx := context.Background()

	createManualMDMEnrollWithOrbit := func(secret string, doUserEnroll bool) (*fleet.Host, *fleet.NanoEnrollment, *mdmtest.TestAppleMDMClient) {
		// orbit enrollment happens before mdm enrollment, otherwise the host would
		// always receive the "no team" profiles on mdm enrollment since it would
		// not be part of any team yet (team assignment is done when it enrolls
		// with orbit).
		mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
			SCEPChallenge: s.scepChallenge,
			SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
			MDMURL:        s.server.URL + apple_mdm.MDMPath,
		}, "MacBookPro16,1")

		// enroll the device with orbit
		var resp EnrollOrbitResponse
		s.DoJSON("POST", "/api/fleet/orbit/enroll", contract.EnrollOrbitRequest{
			EnrollSecret:   secret,
			HardwareUUID:   mdmDevice.UUID, // will not match any existing host
			HardwareSerial: mdmDevice.SerialNumber,
		}, http.StatusOK, &resp)
		require.NotEmpty(t, resp.OrbitNodeKey)
		orbitNodeKey := resp.OrbitNodeKey
		h, err := s.ds.LoadHostByOrbitNodeKey(ctx, orbitNodeKey)
		require.NoError(t, err)
		h.OrbitNodeKey = &orbitNodeKey
		h.Platform = "darwin"

		err = mdmDevice.Enroll()
		require.NoError(t, err)

		var userEnrollment *fleet.NanoEnrollment
		if doUserEnroll {
			// Do a user enrollment with a bit of extra sanity checking
			userEnrollment, err = s.ds.GetNanoMDMUserEnrollment(ctx, h.UUID)
			require.NoError(t, err)
			require.Nil(t, userEnrollment)

			err = mdmDevice.UserEnroll()
			require.NoError(t, err)

			userEnrollment, err = s.ds.GetNanoMDMUserEnrollment(ctx, h.UUID)
			require.NoError(t, err)
			require.NotNil(t, userEnrollment)

		}

		return h, userEnrollment, mdmDevice
	}

	triggerReconcileProfilesMarkVerifying := func() {
		t.Logf("[TestHostMDMAppleProfilesStatus] Calling awaitTriggerProfileSchedule at %s", time.Now().Format(time.RFC3339))
		s.awaitTriggerProfileSchedule(t)
		t.Logf("[TestHostMDMAppleProfilesStatus] awaitTriggerProfileSchedule completed, updating profiles to verifying at %s", time.Now().Format(time.RFC3339))
		// this will only mark them as "pending", as the response to confirm
		// profile deployment is asynchronous, so we simulate it here by
		// updating any "pending" (not NULL) profiles to "verifying"
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			_, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending)
			return err
		})
		t.Logf("[TestHostMDMAppleProfilesStatus] Profiles updated to verifying status at %s", time.Now().Format(time.RFC3339))
	}

	assignHostToTeam := func(h *fleet.Host, teamID *uint) {
		var moveHostResp addHostsToTeamResponse
		s.DoJSON("POST", "/api/v1/fleet/hosts/transfer",
			addHostsToTeamRequest{TeamID: teamID, HostIDs: []uint{h.ID}}, http.StatusOK, &moveHostResp)

		h.TeamID = teamID
	}

	// add a couple global profiles
	payloadScopeSystem := fleet.PayloadScopeSystem
	payloadScopeUser := fleet.PayloadScopeUser
	globalProfiles := [][]byte{
		mobileconfigForTest("G1", "G1"),
		scopedMobileconfigForTest("G2", "G2", &payloadScopeSystem),
		scopedMobileconfigForTest("G3", "G3.user", &payloadScopeUser),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
	// create the no-team enroll secret
	var applyResp applyEnrollSecretSpecResponse
	globalEnrollSec := "global_enroll_sec"
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret",
		applyEnrollSecretSpecRequest{
			Spec: &fleet.EnrollSecretSpec{
				Secrets: []*fleet.EnrollSecret{{Secret: globalEnrollSec}},
			},
		}, http.StatusOK, &applyResp)

	// create a team with a couple profiles
	tm1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team_profiles_status_1"})
	require.NoError(t, err)
	tm1Profiles := [][]byte{
		mobileconfigForTest("T1.1", "T1.1"),
		scopedMobileconfigForTest("T1.2", "T1.2", &payloadScopeSystem),
		scopedMobileconfigForTest("T1.3", "T1.3.user", &payloadScopeUser),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: tm1Profiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm1.ID))
	// create the team 1 enroll secret
	var teamResp teamEnrollSecretsResponse
	tm1EnrollSec := "team1_enroll_sec"
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/secrets", tm1.ID),
		modifyTeamEnrollSecretsRequest{
			Secrets: []fleet.EnrollSecret{{Secret: tm1EnrollSec}},
		}, http.StatusOK, &teamResp)

	// create another team with different profiles
	tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team_profiles_status_2"})
	require.NoError(t, err)
	tm2Profiles := [][]byte{
		mobileconfigForTest("T2.1", "T2.1"),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: tm2Profiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm2.ID))

	// enroll a couple hosts in no team
	h1, h1UserEnrollment, _ := createManualMDMEnrollWithOrbit(globalEnrollSec, true)
	require.Nil(t, h1.TeamID)
	h2, _, _ := createManualMDMEnrollWithOrbit(globalEnrollSec, false)
	require.Nil(t, h2.TeamID)
	// run the cron
	t.Logf("[TestHostMDMAppleProfilesStatus] Starting FIRST cron run (after h1, h2 enrolled) at %s", time.Now().Format(time.RFC3339))
	s.awaitTriggerProfileSchedule(t)
	t.Logf("[TestHostMDMAppleProfilesStatus] FIRST cron run completed at %s", time.Now().Format(time.RFC3339))

	// G3 is user-scoped and the h2 host doesn't have a user-channel yet (and
	// enrolled just now, so the minimum delay to give up and fail the profile
	// delivery is not reached)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		h2: {
			{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	// Verify there is a command on the user channel for h1
	enrollmentIds, err := s.ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx)
	require.NoError(t, err)
	assert.Contains(t, enrollmentIds, h1UserEnrollment.ID)
	assert.Contains(t, enrollmentIds, h1.UUID)
	assert.Contains(t, enrollmentIds, h2.UUID)

	// enroll a couple hosts in team 1
	h3, h3UserEnrollment, _ := createManualMDMEnrollWithOrbit(tm1EnrollSec, true)
	require.NotNil(t, h3.TeamID)
	require.Equal(t, tm1.ID, *h3.TeamID)
	h4, _, h4Device := createManualMDMEnrollWithOrbit(tm1EnrollSec, false)
	require.NotNil(t, h4.TeamID)
	require.Equal(t, tm1.ID, *h4.TeamID)

	// run the cron
	t.Logf("[TestHostMDMAppleProfilesStatus] Starting SECOND cron run (after h3, h4 enrolled in team1) at %s", time.Now().Format(time.RFC3339))
	s.awaitTriggerProfileSchedule(t)
	t.Logf("[TestHostMDMAppleProfilesStatus] SECOND cron run completed at %s", time.Now().Format(time.RFC3339))

	// T1.3 is user-scoped and the h4 host doesn't have a user-channel yet (and
	// enrolled just now, so the minimum delay to give up and send the
	// user-scoped profiles to the device channel is not reached)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h3: {
			{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		h4: {
			{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	// apply the pending profiles
	triggerReconcileProfilesMarkVerifying()

	// Verify there is a command on the user channel for h3
	enrollmentIds, err = s.ds.GetEnrollmentIDsWithPendingMDMAppleCommands(ctx)
	require.NoError(t, err)
	assert.Contains(t, enrollmentIds, h3UserEnrollment.ID)
	assert.Contains(t, enrollmentIds, h3.UUID)
	assert.Contains(t, enrollmentIds, h4.UUID)

	// switch a no team host (h1) to a team (tm2)
	t.Logf("[TestHostMDMAppleProfilesStatus] Transferring host h1 (id=%d) from no team to team tm2 (id=%d) at %s", h1.ID, tm2.ID, time.Now().Format(time.RFC3339))
	var moveHostResp addHostsToTeamResponse
	s.DoJSON("POST", "/api/v1/fleet/hosts/transfer",
		addHostsToTeamRequest{TeamID: &tm2.ID, HostIDs: []uint{h1.ID}}, http.StatusOK, &moveHostResp)
	t.Logf("[TestHostMDMAppleProfilesStatus] Host h1 transfer completed at %s", time.Now().Format(time.RFC3339))
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: "G1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h2: {
			{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// switch a team host (h3) to another team (tm2)
	s.DoJSON("POST", "/api/v1/fleet/hosts/transfer",
		addHostsToTeamRequest{TeamID: &tm2.ID, HostIDs: []uint{h3.ID}}, http.StatusOK, &moveHostResp)

	// create the user-enrollment for host h4
	err = h4Device.UserEnroll()
	require.NoError(t, err)

	// run the cron
	t.Logf("[TestHostMDMAppleProfilesStatus] Starting THIRD cron run (after h3->tm2, h4 user enrolled) at %s", time.Now().Format(time.RFC3339))
	s.awaitTriggerProfileSchedule(t)
	t.Logf("[TestHostMDMAppleProfilesStatus] THIRD cron run completed at %s", time.Now().Format(time.RFC3339))
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h3: {
			{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h4: {
			{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// switch a team host (h4) to no team
	t.Logf("[TestHostMDMAppleProfilesStatus] Transferring host h4 (id=%d) from team tm1 to NO TEAM (nil) at %s", h4.ID, time.Now().Format(time.RFC3339))
	s.DoJSON("POST", "/api/v1/fleet/hosts/transfer",
		addHostsToTeamRequest{TeamID: nil, HostIDs: []uint{h4.ID}}, http.StatusOK, &moveHostResp)
	t.Logf("[TestHostMDMAppleProfilesStatus] Host h4 transfer to NO TEAM completed at %s", time.Now().Format(time.RFC3339))
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h3: {
			{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h4: {
			{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, // still pending install due to cron not having run
			{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// apply the pending profiles
	triggerReconcileProfilesMarkVerifying()

	// add a profile to no team (h2 and h4 are now part of no team)
	body, headers := generateNewProfileMultipartRequest(t,
		"some_name", mobileconfigForTest("G4", "G4"), s.token, nil)
	s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusOK, headers)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h2: { // still no user channel
			{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		h4: {
			{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	// add a profile to team 2 (h1 and h3 are now part of team 2)
	body, headers = generateNewProfileMultipartRequest(t,
		"some_name", mobileconfigForTest("T2.2", "T2.2"), s.token, map[string][]string{"team_id": {fmt.Sprintf("%d", tm2.ID)}})
	s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusOK, headers)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "T2.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h3: {
			{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "T2.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// apply the pending profiles
	triggerReconcileProfilesMarkVerifying()

	// delete a no team profile
	noTeamProfs, err := s.ds.ListMDMAppleConfigProfiles(ctx, nil)
	require.NoError(t, err)
	var g1ProfID uint
	for _, p := range noTeamProfs {
		if p.Identifier == "G1" {
			g1ProfID = p.ProfileID
			break
		}
	}
	require.NotZero(t, g1ProfID)
	var delProfResp deleteMDMAppleConfigProfileResponse
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", g1ProfID),
		deleteMDMAppleConfigProfileRequest{}, http.StatusOK, &delProfResp)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h2: {
			{Identifier: "G1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h4: {
			{Identifier: "G1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// delete a team profile
	tm2Profs, err := s.ds.ListMDMAppleConfigProfiles(ctx, &tm2.ID)
	require.NoError(t, err)
	var tm21ProfID uint
	for _, p := range tm2Profs {
		if p.Identifier == "T2.1" {
			tm21ProfID = p.ProfileID
			break
		}
	}
	require.NotZero(t, tm21ProfID)
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", tm21ProfID),
		deleteMDMAppleConfigProfileRequest{}, http.StatusOK, &delProfResp)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h3: {
			{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// apply the pending profiles
	triggerReconcileProfilesMarkVerifying()

	// bulk-set profiles for no team, with add/delete/edit
	g2Edited := mobileconfigForTest("G2b", "G2b")
	g5Content := mobileconfigForTest("G5", "G5")
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{
			Profiles: [][]byte{
				g2Edited,
				// G3 is deleted
				// G4 is deleted
				g5Content,
			},
		}, http.StatusNoContent)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h2: {
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G4", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h4: {
			{Identifier: "G2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G4", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// bulk-set profiles for a team, with add/delete/edit
	t22Edited := mobileconfigForTest("T2.2b", "T2.2b")
	t23Content := mobileconfigForTest("T2.3", "T2.3")
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{
			Profiles: [][]byte{
				t22Edited,
				t23Content,
			},
		}, http.StatusNoContent, "team_id", fmt.Sprint(tm2.ID))
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: "T2.2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h3: {
			{Identifier: "T2.2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// apply the pending profiles
	triggerReconcileProfilesMarkVerifying()

	// bulk-set profiles for no team and team 2, without changes, and team 1 added (but no host affected)
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{
			Profiles: [][]byte{
				g2Edited,
				g5Content,
			},
		}, http.StatusNoContent)
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{
			Profiles: [][]byte{
				t22Edited,
				t23Content,
			},
		}, http.StatusNoContent, "team_id", fmt.Sprint(tm2.ID))
	s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{
			Profiles: [][]byte{
				scopedMobileconfigForTest("T1.3", "T1.3.user", &payloadScopeUser),
			},
		}, http.StatusNoContent, "team_id", fmt.Sprint(tm1.ID))
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: "T2.2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "T2.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h2: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h3: {
			{Identifier: "T2.2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "T2.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h4: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// delete team 2 (h1 and h3 are part of that team)
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/teams/%d", tm2.ID), nil, http.StatusOK)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: "T2.2b", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h3: {
			{Identifier: "T2.2b", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "T2.3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// apply the pending profiles
	triggerReconcileProfilesMarkVerifying()

	// all profiles now verifying
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h2: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h3: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h4: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// h1 verified one of the profiles
	require.NoError(t, apple_mdm.VerifyHostMDMProfiles(context.Background(), s.ds, h1, map[string]*fleet.HostMacOSProfile{
		"G2b": {Identifier: "G2b", DisplayName: "G2b", InstallDate: time.Now()},
	}))
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerified},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h2: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h3: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h4: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// switch a team host (h1) to another team (tm1)
	assignHostToTeam(h1, &tm1.ID)

	// Create a new profile that will be labeled
	body, headers = generateNewProfileMultipartRequest(
		t,
		"label_prof",
		mobileconfigForTest("label_prof", "label_prof"),
		s.token,
		map[string][]string{"team_id": {fmt.Sprintf("%d", tm1.ID)}},
	)
	s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusOK, headers)

	var uid string
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`, "label_prof")
	})

	label, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "test label 1", Query: "select 1;"})
	require.NoError(t, err)

	// Update label with host membership
	mysql.ExecAdhocSQL(
		t, s.ds, func(db sqlx.ExtContext) error {
			_, err := db.ExecContext(
				context.Background(),
				"INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, ?)",
				h1.ID,
				label.ID,
			)
			return err
		},
	)

	// Update profile <-> label mapping
	mysql.ExecAdhocSQL(
		t, s.ds, func(db sqlx.ExtContext) error {
			_, err := db.ExecContext(
				context.Background(),
				"INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)",
				uid,
				label.Name,
				label.ID,
			)
			return err
		},
	)

	triggerReconcileProfilesMarkVerifying()

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "label_prof", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h2: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h3: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h4: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	require.NoError(t, apple_mdm.VerifyHostMDMProfiles(context.Background(), s.ds, h1, map[string]*fleet.HostMacOSProfile{
		"label_prof": {Identifier: "label_prof", DisplayName: "label_prof", InstallDate: time.Now()},
	}))

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		h1: {
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "label_prof", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerified},
		},
		h2: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h3: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		h4: {
			{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
}

func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
	t := s.T()
	ctx := context.Background()

	testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"})
	require.NoError(t, err)

	// Ensure MDM is turned on
	appConfig, err := s.ds.AppConfig(ctx)
	require.NoError(t, err)
	appConfig.MDM.AndroidEnabledAndConfigured = true
	appConfig.MDM.EnabledAndConfigured = true
	appConfig.MDM.WindowsEnabledAndConfigured = true
	err = s.ds.SaveAppConfig(ctx, appConfig)
	require.NoError(t, err)

	// NOTE: label names starting with "-" are sent as "labels_excluding_any"
	// (and the leading "-" is removed from the name). Names starting with
	// "!" are sent as the deprecated "labels" field (and the "!" is removed).
	// Names starting with a "~" prefix are sent as "labels_include_any"
	// (and the leading "~" is removed.
	addLabelsFields := func(labelNames []string) map[string][]string {
		var deprLabels, inclAllLabels, inclAnyLabels, exclLabels []string
		for _, lbl := range labelNames {
			switch {
			case strings.HasPrefix(lbl, "~"):
				inclAnyLabels = append(inclAnyLabels, strings.TrimPrefix(lbl, "~"))
			case strings.HasPrefix(lbl, "-"):
				exclLabels = append(exclLabels, strings.TrimPrefix(lbl, "-"))
			case strings.HasPrefix(lbl, "!"):
				deprLabels = append(deprLabels, strings.TrimPrefix(lbl, "!"))
			default:
				inclAllLabels = append(inclAllLabels, lbl)
			}
		}

		fields := make(map[string][]string)
		if len(deprLabels) > 0 {
			fields["labels"] = deprLabels
		}
		if len(inclAllLabels) > 0 {
			fields["labels_include_all"] = inclAllLabels
		}
		if len(exclLabels) > 0 {
			fields["labels_exclude_any"] = exclLabels
		}
		if len(inclAnyLabels) > 0 {
			fields["labels_include_any"] = inclAnyLabels
		}
		return fields
	}

	assertAppleProfile := func(filename, name, ident string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string {
		fields := addLabelsFields(labelNames)
		if teamID > 0 {
			fields["team_id"] = []string{fmt.Sprintf("%d", teamID)}
		}
		body, headers := generateNewProfileMultipartRequest(
			t, filename, mobileconfigForTest(name, ident), s.token, fields,
		)
		res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), wantStatus, headers)

		if wantErrMsg != "" {
			errMsg := extractServerErrorText(res.Body)
			require.Contains(t, errMsg, wantErrMsg)
			return ""
		}

		var resp newMDMConfigProfileResponse
		err := json.NewDecoder(res.Body).Decode(&resp)
		require.NoError(t, err)
		require.NotEmpty(t, resp.ProfileUUID)
		require.Equal(t, "a", string(resp.ProfileUUID[0]))
		return resp.ProfileUUID
	}
	assertAppleDeclaration := func(filename, ident string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string {
		fields := addLabelsFields(labelNames)
		if teamID > 0 {
			fields["team_id"] = []string{fmt.Sprintf("%d", teamID)}
		}

		bytes := []byte(fmt.Sprintf(`{
  "Type": "com.apple.configuration.foo",
  "Payload": {
    "Echo": "f1337"
  },
  "Identifier": "%s"
}`, ident))

		body, headers := generateNewProfileMultipartRequest(t, filename, bytes, s.token, fields)
		res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), wantStatus, headers)

		if wantErrMsg != "" {
			errMsg := extractServerErrorText(res.Body)
			require.Contains(t, errMsg, wantErrMsg)
			return ""
		}

		var resp newMDMConfigProfileResponse
		err := json.NewDecoder(res.Body).Decode(&resp)
		require.NoError(t, err)
		require.NotEmpty(t, resp.ProfileUUID)
		require.Equal(t, fleet.MDMAppleDeclarationUUIDPrefix, string(resp.ProfileUUID[0]))
		return resp.ProfileUUID
	}

	createAppleProfile := func(name, ident string, teamID uint, labelNames []string) string {
		uid := assertAppleProfile(name+".mobileconfig", name, ident, teamID, labelNames, http.StatusOK, "")

		var wantJSON string
		if teamID == 0 {
			wantJSON = fmt.Sprintf(`{"team_id": null, "team_name": null, "profile_name": %q, "profile_identifier": %q}`, name, ident)
		} else {
			wantJSON = fmt.Sprintf(`{"team_id": %d, "team_name": %q, "profile_name": %q, "profile_identifier": %q}`, teamID, testTeam.Name, name, ident)
		}
		s.lastActivityOfTypeMatches(fleet.ActivityTypeCreatedMacosProfile{}.ActivityName(), wantJSON, 0)

		return uid
	}

	createAppleDeclaration := func(name, ident string, teamID uint, labelNames []string) string {
		uid := assertAppleDeclaration(name+".json", ident, teamID, labelNames, http.StatusOK, "")

		var wantJSON string
		if teamID == 0 {
			wantJSON = fmt.Sprintf(`{"team_id": null, "team_name": null, "profile_name": %q, "identifier": %q}`, name, ident)
		} else {
			wantJSON = fmt.Sprintf(`{"team_id": %d, "team_name": %q, "profile_name": %q, "identifier": %q}`, teamID, testTeam.Name, name, ident)
		}
		s.lastActivityOfTypeMatches(fleet.ActivityTypeCreatedDeclarationProfile{}.ActivityName(), wantJSON, 0)

		return uid
	}

	assertWindowsProfile := func(filename, locURI string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string {
		fields := addLabelsFields(labelNames)
		if teamID > 0 {
			fields["team_id"] = []string{fmt.Sprintf("%d", teamID)}
		}
		body, headers := generateNewProfileMultipartRequest(
			t,
			filename,
			[]byte(fmt.Sprintf(`<Add><Item><Target><LocURI>%s</LocURI></Target></Item></Add><Replace><Item><Target><LocURI>%s</LocURI></Target></Item></Replace>`, locURI, locURI)),
			s.token,
			fields,
		)
		res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), wantStatus, headers)

		if wantErrMsg != "" {
			errMsg := extractServerErrorText(res.Body)
			require.Contains(t, errMsg, wantErrMsg)
			return ""
		}

		var resp newMDMConfigProfileResponse
		err := json.NewDecoder(res.Body).Decode(&resp)
		require.NoError(t, err)
		require.NotEmpty(t, resp.ProfileUUID)
		require.Equal(t, "w", string(resp.ProfileUUID[0]))
		return resp.ProfileUUID
	}
	createWindowsProfile := func(name string, teamID uint, labels []string) string {
		uid := assertWindowsProfile(name+".xml", "./Test", teamID, labels, http.StatusOK, "")

		var wantJSON string
		if teamID == 0 {
			wantJSON = fmt.Sprintf(`{"team_id": null, "team_name": null, "profile_name": %q}`, name)
		} else {
			wantJSON = fmt.Sprintf(`{"team_id": %d, "team_name": %q, "profile_name": %q}`, teamID, testTeam.Name, name)
		}
		s.lastActivityOfTypeMatches(fleet.ActivityTypeCreatedWindowsProfile{}.ActivityName(), wantJSON, 0)

		return uid
	}

	assertAndroidProfile := func(filename string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string {
		fields := addLabelsFields(labelNames)
		if teamID > 0 {
			fields["team_id"] = []string{fmt.Sprintf("%d", teamID)}
		}

		bytes := []byte(`{
  "removeUserDisabled": false
}`)

		body, headers := generateNewProfileMultipartRequest(t, filename, bytes, s.token, fields)
		res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), wantStatus, headers)

		if wantErrMsg != "" {
			errMsg := extractServerErrorText(res.Body)
			require.Contains(t, errMsg, wantErrMsg)
			return ""
		}

		var resp newMDMConfigProfileResponse
		err := json.NewDecoder(res.Body).Decode(&resp)
		require.NoError(t, err)
		require.NotEmpty(t, resp.ProfileUUID)
		require.Equal(t, fleet.MDMAndroidProfileUUIDPrefix, string(resp.ProfileUUID[0]))
		return resp.ProfileUUID
	}

	createAndroidProfile := func(name string, teamID uint, labelNames []string) string {
		uid := assertAndroidProfile(name+".json", teamID, labelNames, http.StatusOK, "")

		var wantJSON string
		if teamID == 0 {
			wantJSON = fmt.Sprintf(`{"team_id": null, "team_name": null, "profile_name": %q}`, name)
		} else {
			wantJSON = fmt.Sprintf(`{"team_id": %d, "team_name": %q, "profile_name": %q}`, teamID, testTeam.Name, name)
		}
		s.lastActivityOfTypeMatches(fleet.ActivityTypeCreatedAndroidProfile{}.ActivityName(), wantJSON, 0)

		return uid
	}

	// create a couple Apple profiles for no-team and team
	noTeamAppleProfUUID := createAppleProfile("apple-global-profile", "test-global-ident", 0, nil)
	teamAppleProfUUID := createAppleProfile("apple-team-profile", "test-team-ident", testTeam.ID, nil)
	// create a couple Windows profiles for no-team and team
	noTeamWinProfUUID := createWindowsProfile("win-global-profile", 0, nil)
	teamWinProfUUID := createWindowsProfile("win-team-profile", testTeam.ID, nil)
	// Create a couple Android profiles for no-team and team
	noTeamAndroidProfUUID := createAndroidProfile("android-global-profile", 0, nil)
	teamAndroidProfUUID := createAndroidProfile("android-team-profile", testTeam.ID, nil)

	// Windows profile name conflicts with Apple's for no team
	assertWindowsProfile("apple-global-profile.xml", "./Test", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// but no conflict for team 1
	assertWindowsProfile("apple-global-profile.xml", "./Test", testTeam.ID, nil, http.StatusOK, "")
	// Apple profile name conflicts with Windows' for no team
	assertAppleProfile("win-global-profile.mobileconfig", "win-global-profile", "test-global-ident-2", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// but no conflict for team 1
	assertAppleProfile("win-global-profile.mobileconfig", "win-global-profile", "test-global-ident-2", testTeam.ID, nil, http.StatusOK, "")
	// Windows profile name conflicts with Apple's for team 1
	assertWindowsProfile("apple-team-profile.xml", "./Test", testTeam.ID, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// but no conflict for no-team
	assertWindowsProfile("apple-team-profile.xml", "./Test", 0, nil, http.StatusOK, "")
	// Apple profile name conflicts with Windows' for team 1
	assertAppleProfile("win-team-profile.mobileconfig", "win-team-profile", "test-team-ident-2", testTeam.ID, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// but no conflict for no-team
	assertAppleProfile("win-team-profile.mobileconfig", "win-team-profile", "test-team-ident-2", 0, nil, http.StatusOK, "")
	// Android profile name conflicts with Apple's for no team
	assertAndroidProfile("apple-global-profile.json", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)

	// add some macOS declarations
	createAppleDeclaration("apple-declaration", "test-declaration-ident", 0, nil)
	// identifier must be unique, it conflicts with existing declaration
	assertAppleDeclaration("apple-declaration.json", "test-declaration-ident", 0, nil, http.StatusConflict, "test-declaration-ident already exists")
	// name is pulled from filename, it conflicts with existing declaration
	assertAppleDeclaration("apple-declaration.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// uniqueness is checked only within team, so it's fine to have the same name and identifier in different teams
	assertAppleDeclaration("apple-declaration.json", "test-declaration-ident", testTeam.ID, nil, http.StatusOK, "")
	// name is pulled from filename, it conflicts with existing macOS config profile
	assertAppleDeclaration("apple-global-profile.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// name is pulled from filename, it conflicts with existing macOS config profile
	assertAppleDeclaration("win-global-profile.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// name is pulled from filename, it conflicts with existing Android config profile
	assertAppleDeclaration("android-global-profile.json", "test-declaration-ident-2", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// windows profile name conflicts with existing declaration
	assertWindowsProfile("apple-declaration.xml", "./Test", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// macOS profile name conflicts with existing declaration
	assertAppleProfile("apple-declaration.mobileconfig", "apple-declaration", "test-declaration-ident", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)
	// Android profile name conflicts with existing declaration
	assertAndroidProfile("apple-declaration.json", 0, nil, http.StatusConflict, SameProfileNameUploadErrorMsg)

	// not an xml nor mobileconfig file
	assertWindowsProfile("foo.txt", "./Test", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.")
	assertAppleProfile("foo.txt", "foo", "foo-ident", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.")
	assertAppleDeclaration("foo.txt", "foo-ident", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.")
	assertAndroidProfile("foo.txt", 0, nil, http.StatusBadRequest, "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file.")

	// Windows-reserved LocURI
	assertWindowsProfile("bitlocker.xml", syncml.FleetBitLockerTargetLocURI, 0, nil, http.StatusBadRequest,
		syncml.DiskEncryptionProfileRestrictionErrMsg)
	assertWindowsProfile("updates.xml", syncml.FleetOSUpdateTargetLocURI, testTeam.ID, nil, http.StatusBadRequest,
		"Couldn't add. Custom configuration profiles can't include Windows updates settings.")

	// Fleet-reserved profiles
	for name := range servermdm.FleetReservedProfileNames() {
		assertAppleProfile(name+".mobileconfig", name, name+"-ident", 0, nil, http.StatusBadRequest, fmt.Sprintf(`name %s is not allowed`, name))
		assertAppleDeclaration(name+".json", name+"-ident", 0, nil, http.StatusBadRequest, fmt.Sprintf(`name %q is not allowed`, name))
		assertWindowsProfile(name+".xml", "./Test", 0, nil, http.StatusBadRequest, fmt.Sprintf(`Couldn't add. Profile name %q is not allowed.`, name))
	}

	// profiles with non-existent labels
	assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist")
	assertAppleDeclaration("apple-declaration-with-labels.json", "ident-with-labels", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist")
	assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist")
	assertAndroidProfile("android-with-labels.json", 0, []string{"does-not-exist"}, http.StatusBadRequest, "some or all the labels provided don't exist")

	// create a couple of labels
	labelFoo := &fleet.Label{Name: "foo", Query: "select * from foo;"}
	labelFoo, err = s.ds.NewLabel(context.Background(), labelFoo)
	require.NoError(t, err)
	labelBar := &fleet.Label{Name: "bar", Query: "select * from bar;"}
	labelBar, err = s.ds.NewLabel(context.Background(), labelBar)
	require.NoError(t, err)

	// profiles mixing existent and non-existent labels
	assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"does-not-exist", "foo"}, http.StatusBadRequest, "some or all the labels provided don't exist")
	assertAppleDeclaration("apple-declaration-with-labels.json", "ident-with-labels", 0, []string{"does-not-exist", "foo"}, http.StatusBadRequest, "some or all the labels provided don't exist")
	assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"does-not-exist", "bar"}, http.StatusBadRequest, "some or all the labels provided don't exist")
	assertAndroidProfile("android-profile-with-labels.json", 0, []string{"does-not-exist", "bar"}, http.StatusBadRequest, "some or all the labels provided don't exist")

	// profiles with invalid mix of labels
	assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
	assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
	assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "-bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
	assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
	assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
	assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
	assertAndroidProfile("android-invalid-profile-with-labels.json", 0, []string{"-foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
	assertAndroidProfile("android-invalid-profile-with-labels.json", 0, []string{"-foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)

	// profiles with valid labels
	uuidAppleWithLabel := assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"!foo"}, http.StatusOK, "")
	uuidAppleWithInclAnyLabel := assertAppleProfile("apple-profile-with-incl-any-labels.mobileconfig", "apple-profile-with-incl-any-labels", "ident-with-incl-any-labels", 0, []string{"~foo", "~bar"}, http.StatusOK, "")
	uuidAppleDDMWithLabel := createAppleDeclaration("apple-decl-with-labels", "ident-decl-with-labels", 0, []string{"foo"})
	uuidWindowsWithLabel := assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"-foo", "-bar"}, http.StatusOK, "")
	uuidAppleDDMTeamWithLabel := createAppleDeclaration("apple-team-decl-with-labels", "ident-team-decl-with-labels", testTeam.ID, []string{"-foo"})
	uuidWindowsTeamWithLabel := assertWindowsProfile("win-team-profile-with-labels.xml", "./Test", testTeam.ID, []string{"foo", "bar"}, http.StatusOK, "")
	uuidWindowsTeamWithInclAnyLabel := assertWindowsProfile("win-team-profile-with-incl-any-labels.xml", "./Test", testTeam.ID, []string{"~foo", "~bar"}, http.StatusOK, "")

	uuidAndroidWithLabel := assertAndroidProfile("android-profile-with-labels.json", 0, []string{"-foo", "-bar"}, http.StatusOK, "")
	uuidAndroidTeamWithLabel := assertAndroidProfile("android-team-profile-with-labels.json", testTeam.ID, []string{"foo", "bar"}, http.StatusOK, "")
	uuidAndroidTeamWithInclAnyLabel := assertAndroidProfile("android-team-profile-with-incl-any-labels.json", testTeam.ID, []string{"~foo", "~bar"}, http.StatusOK, "")

	// Windows invalid content
	body, headers := generateNewProfileMultipartRequest(t, "win.xml", []byte("\x00\x01\x02"), s.token, nil)
	res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusBadRequest, headers)
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Couldn't add. The file should include valid XML:")

	// Apple invalid mobileconfig content
	body, headers = generateNewProfileMultipartRequest(t,
		"apple.mobileconfig", []byte("\x00\x01\x02"), s.token, nil)
	res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusBadRequest, headers)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Configuration profiles can't be signed. Fleet wil sign the profile for you.")

	// Apple/Android invalid json declaration
	body, headers = generateNewProfileMultipartRequest(t,
		"apple.json", []byte("{"), s.token, nil)
	res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusBadRequest, headers)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Couldn't add. The file should include valid JSON:")

	// Apple/Android cannot determine which
	body, headers = generateNewProfileMultipartRequest(t,
		"apple_or_android.json", []byte(`{"lower_key": true,"UpperKey": false}`), s.token, nil)
	res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusBadRequest, headers)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Keys in declaration (DDM) profile must contain only letters and start with a uppercase letter. Keys in Android profile must contain only letters and start with a lowercase letter.")

	// Android invalid keys
	for key, expectedErr := range fleet.AndroidForbiddenJSONKeys {
		body, headers = generateNewProfileMultipartRequest(t,
			"android.json", []byte(fmt.Sprintf(`{"%s": true}`, key)), s.token, nil)
		res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusBadRequest, headers)
		errMsg = extractServerErrorText(res.Body)
		require.Contains(t, errMsg, expectedErr)
	}

	// get the existing profiles work
	expectedProfiles := []fleet.MDMConfigProfilePayload{
		{ProfileUUID: noTeamAppleProfUUID, Platform: "darwin", Name: "apple-global-profile", Identifier: "test-global-ident", TeamID: nil, Scope: string(fleet.PayloadScopeSystem)},
		{ProfileUUID: teamAppleProfUUID, Platform: "darwin", Name: "apple-team-profile", Identifier: "test-team-ident", TeamID: &testTeam.ID, Scope: string(fleet.PayloadScopeSystem)},
		{ProfileUUID: noTeamWinProfUUID, Platform: "windows", Name: "win-global-profile", TeamID: nil},
		{ProfileUUID: teamWinProfUUID, Platform: "windows", Name: "win-team-profile", TeamID: &testTeam.ID},
		{ProfileUUID: noTeamAndroidProfUUID, Platform: "android", Name: "android-global-profile", TeamID: nil},
		{ProfileUUID: teamAndroidProfUUID, Platform: "android", Name: "android-team-profile", TeamID: &testTeam.ID},
		{
			ProfileUUID: uuidAppleDDMWithLabel, Platform: "darwin", Name: "apple-decl-with-labels", Identifier: "ident-decl-with-labels", TeamID: nil,
			LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
		{
			ProfileUUID: uuidAppleWithLabel, Platform: "darwin", Name: "apple-profile-with-labels", Identifier: "ident-with-labels", TeamID: nil, Scope: string(fleet.PayloadScopeSystem),
			LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
		{
			ProfileUUID: uuidAppleWithInclAnyLabel, Platform: "darwin", Name: "apple-profile-with-incl-any-labels", Identifier: "ident-with-incl-any-labels", TeamID: nil, Scope: string(fleet.PayloadScopeSystem),
			LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
				{LabelID: labelBar.ID, LabelName: labelBar.Name},
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
		{
			ProfileUUID: uuidWindowsWithLabel, Platform: "windows", Name: "win-profile-with-labels", TeamID: nil,
			LabelsExcludeAny: []fleet.ConfigurationProfileLabel{
				{LabelID: labelBar.ID, LabelName: labelBar.Name},
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
		{
			ProfileUUID: uuidAppleDDMTeamWithLabel, Platform: "darwin", Name: "apple-team-decl-with-labels", Identifier: "ident-team-decl-with-labels", TeamID: &testTeam.ID,
			LabelsExcludeAny: []fleet.ConfigurationProfileLabel{
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
		{
			ProfileUUID: uuidWindowsTeamWithLabel, Platform: "windows", Name: "win-team-profile-with-labels", TeamID: &testTeam.ID,
			LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
				{LabelID: labelBar.ID, LabelName: labelBar.Name},
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
		{
			ProfileUUID: uuidWindowsTeamWithInclAnyLabel, Platform: "windows", Name: "win-team-profile-with-incl-any-labels", TeamID: &testTeam.ID,
			LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
				{LabelID: labelBar.ID, LabelName: labelBar.Name},
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
		{
			ProfileUUID: uuidAndroidWithLabel, Platform: "android", Name: "android-profile-with-labels", TeamID: nil,
			LabelsExcludeAny: []fleet.ConfigurationProfileLabel{
				{LabelID: labelBar.ID, LabelName: labelBar.Name},
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
		{
			ProfileUUID: uuidAndroidTeamWithLabel, Platform: "android", Name: "android-team-profile-with-labels", TeamID: &testTeam.ID,
			LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
				{LabelID: labelBar.ID, LabelName: labelBar.Name},
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
		{
			ProfileUUID: uuidAndroidTeamWithInclAnyLabel, Platform: "android", Name: "android-team-profile-with-incl-any-labels", TeamID: &testTeam.ID,
			LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
				{LabelID: labelBar.ID, LabelName: labelBar.Name},
				{LabelID: labelFoo.ID, LabelName: labelFoo.Name},
			},
		},
	}
	for _, prof := range expectedProfiles {
		var getResp getMDMConfigProfileResponse
		s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", prof.ProfileUUID), nil, http.StatusOK, &getResp)
		require.NotZero(t, getResp.CreatedAt)
		require.NotZero(t, getResp.UploadedAt)
		if getResp.Platform == "darwin" {
			require.Len(t, getResp.Checksum, 16)
		} else {
			require.Empty(t, getResp.Checksum)
		}
		getResp.CreatedAt, getResp.UploadedAt = time.Time{}, time.Time{}
		getResp.Checksum = nil
		// sort the labels by name
		sort.Slice(getResp.LabelsIncludeAll, func(i, j int) bool {
			return getResp.LabelsIncludeAll[i].LabelName < getResp.LabelsIncludeAll[j].LabelName
		})
		sort.Slice(getResp.LabelsExcludeAny, func(i, j int) bool {
			return getResp.LabelsExcludeAny[i].LabelName < getResp.LabelsExcludeAny[j].LabelName
		})
		sort.Slice(getResp.LabelsIncludeAny, func(i, j int) bool {
			return getResp.LabelsIncludeAny[i].LabelName < getResp.LabelsIncludeAny[j].LabelName
		})
		require.Equal(t, prof, *getResp.MDMConfigProfilePayload)

		resp := s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", prof.ProfileUUID), nil, http.StatusOK, "alt", "media")
		require.NotZero(t, resp.ContentLength)
		require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;")
		if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleProfileUUIDPrefix) { //nolint:gocritic // ignore ifElseChain
			require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
		} else if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
			require.Contains(t, resp.Header.Get("Content-Type"), "application/json")
		} else if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAndroidProfileUUIDPrefix) {
			require.Contains(t, resp.Header.Get("Content-Type"), "application/json")
		} else {
			require.Contains(t, resp.Header.Get("Content-Type"), "application/octet-stream")
		}
		require.Contains(t, resp.Header.Get("X-Content-Type-Options"), "nosniff")

		b, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		require.Equal(t, resp.ContentLength, int64(len(b)))
	}

	var getResp getMDMConfigProfileResponse
	// get an unknown Apple profile
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "ano-such-profile"), nil, http.StatusNotFound, &getResp)
	s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "ano-such-profile"), nil, http.StatusNotFound, "alt", "media")
	// get an unknown Apple declaration
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAppleDeclarationUUIDPrefix)), nil, http.StatusNotFound, &getResp)
	s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAppleDeclarationUUIDPrefix)), nil, http.StatusNotFound, "alt", "media")
	// get an unknown Windows profile
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "wno-such-profile"), nil, http.StatusNotFound, &getResp)
	s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "wno-such-profile"), nil, http.StatusNotFound, "alt", "media")
	// get an unknown Android profile
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAndroidProfileUUIDPrefix)), nil, http.StatusNotFound, &getResp)
	s.Do("GET", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAndroidProfileUUIDPrefix)), nil, http.StatusNotFound, "alt", "media")

	var deleteResp deleteMDMConfigProfileResponse
	// delete existing Apple profiles
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", noTeamAppleProfUUID), nil, http.StatusOK, &deleteResp)
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", teamAppleProfUUID), nil, http.StatusOK, &deleteResp)
	// delete non-existing Apple profile
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "ano-such-profile"), nil, http.StatusNotFound, &deleteResp)

	// delete existing Apple declaration
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", uuidAppleDDMWithLabel), nil, http.StatusOK, &deleteResp)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeDeletedDeclarationProfile{}.ActivityName(),
		`{"profile_name": "apple-decl-with-labels", "identifier": "ident-decl-with-labels", "team_id": null, "team_name": null}`,
		0,
	)
	// delete non-existing Apple declaration
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAppleDeclarationUUIDPrefix)), nil, http.StatusNotFound, &deleteResp)
	// delete existing Windows profiles
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", noTeamWinProfUUID), nil, http.StatusOK, &deleteResp)
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", teamWinProfUUID), nil, http.StatusOK, &deleteResp)
	// delete non-existing Windows profile
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", "wno-such-profile"), nil, http.StatusNotFound, &deleteResp)

	// delete existing Android profiles
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", noTeamAndroidProfUUID), nil, http.StatusOK, &deleteResp)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeDeletedAndroidProfile{}.ActivityName(),
		`{"profile_name": "android-global-profile", "team_id": null, "team_name": null}`,
		0,
	)
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", teamAndroidProfUUID), nil, http.StatusOK, &deleteResp)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeDeletedAndroidProfile{}.ActivityName(),
		fmt.Sprintf(`{"profile_name": "android-team-profile", "team_id": %d, "team_name": %q}`, testTeam.ID, testTeam.Name),
		0,
	)
	// delete non-existing Android profiles
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", fmt.Sprintf("%sno-such-profile", fleet.MDMAndroidProfileUUIDPrefix)), nil, http.StatusNotFound, &deleteResp)

	// trying to create/delete profiles managed by Fleet fails
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		assertAppleProfile("foo.mobileconfig", p, p, 0, nil, http.StatusBadRequest, fmt.Sprintf("payload identifier %s is not allowed", p))

		// create it directly in the DB to test deletion
		uid := "a" + uuid.NewString()
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			mc := mcBytesForTest(p, p, uuid.New().String())
			_, err := q.ExecContext(ctx,
				"INSERT INTO mdm_apple_configuration_profiles (profile_uuid, identifier, name, mobileconfig, checksum, team_id, uploaded_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP())",
				uid, p, p, mc, "1234", 0)
			return err
		})

		var deleteResp deleteMDMConfigProfileResponse
		s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", uid), nil, http.StatusBadRequest, &deleteResp)

		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			_, err := q.ExecContext(ctx,
				"DELETE FROM mdm_apple_configuration_profiles WHERE profile_uuid = ?",
				uid)
			return err
		})
	}
	// TODO: Add tests for create/delete forbidden declaration types?

	// make fleet add a FileVault profile
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	profile := s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// try to delete the profile
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profile.ProfileUUID), nil, http.StatusBadRequest, &deleteResp)

	// make fleet add a Windows OS Updates profile
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "windows_updates": {"deadline_days": 1, "grace_period_days": 1} }
  }`), http.StatusOK, &acResp)
	profUUID := checkWindowsOSUpdatesProfile(t, s.ds, nil, &fleet.WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(1)})

	// try to delete the profile
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profUUID), nil, http.StatusBadRequest, &deleteResp)

	// TODO: Add tests for OS updates declaration when implemented.
}

func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() {
	t := s.T()
	ctx := context.Background()

	// create some teams
	tm1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
	require.NoError(t, err)
	tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
	require.NoError(t, err)
	tm3, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team3"})
	require.NoError(t, err)

	// create 5 profiles for no team and team 1, names are A, B, C ... for global and
	// tA, tB, tC ... for team 1. Alternate macOS and Windows profiles.
	for i := 0; i < 5; i++ {
		name := string('A' + byte(i))
		if i%2 == 0 {
			prof, err := fleet.NewMDMAppleConfigProfile(mcBytesForTest(name, name+".identifier", name+".uuid"), nil)
			require.NoError(t, err)
			_, err = s.ds.NewMDMAppleConfigProfile(ctx, *prof, nil)
			require.NoError(t, err)

			tprof, err := fleet.NewMDMAppleConfigProfile(mcBytesForTest("t"+name, "t"+name+".identifier", "t"+name+".uuid"), nil)
			require.NoError(t, err)
			tprof.TeamID = &tm1.ID
			_, err = s.ds.NewMDMAppleConfigProfile(ctx, *tprof, nil)
			require.NoError(t, err)
		} else {
			_, err = s.ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: name, SyncML: []byte(`<Replace></Replace>`)}, nil)
			require.NoError(t, err)
			_, err = s.ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "t" + name, TeamID: &tm1.ID, SyncML: []byte(`<Replace></Replace>`)}, nil)
			require.NoError(t, err)
		}
	}

	lblFoo, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "foo", Query: "select 1"})
	require.NoError(t, err)
	lblBar, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "bar", Query: "select 1"})
	require.NoError(t, err)
	lblBaz, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "baz", Query: "select 1"})
	require.NoError(t, err)

	// create a couple profiles (Win and mac) for team 2, and none for team 3
	tprof, err := fleet.NewMDMAppleConfigProfile(mcBytesForTest("tF", "tF.identifier", "tF.uuid"), nil)
	require.NoError(t, err)
	tprof.TeamID = &tm2.ID
	// make tm2ProfF a "exclude-any" label-based profile
	tprof.LabelsExcludeAny = []fleet.ConfigurationProfileLabel{
		{LabelID: lblFoo.ID, LabelName: lblFoo.Name},
		{LabelID: lblBar.ID, LabelName: lblBar.Name},
	}
	tm2ProfF, err := s.ds.NewMDMAppleConfigProfile(ctx, *tprof, nil)
	require.NoError(t, err)
	// checksum is not returned by New..., so compute it manually
	checkSum := md5.Sum(tm2ProfF.Mobileconfig) // nolint:gosec // used only for test
	tm2ProfF.Checksum = checkSum[:]

	// make tm2ProfG a "include-all" label-based profile
	tm2ProfG, err := s.ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
		Name:   "tG",
		TeamID: &tm2.ID,
		SyncML: []byte(`<Add></Add>`),
		LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
			{LabelID: lblFoo.ID, LabelName: lblFoo.Name},
			{LabelID: lblBar.ID, LabelName: lblBar.Name},
		},
	}, nil)
	require.NoError(t, err)

	// make tm2ProfH a "include-any" label-based profile
	tm2ProfH, err := s.ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
		Name:   "tH",
		TeamID: &tm2.ID,
		SyncML: []byte(`<Add></Add>`),
		LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
			{LabelID: lblBar.ID, LabelName: lblBar.Name},
			{LabelID: lblBaz.ID, LabelName: lblBaz.Name},
		},
	}, nil)
	require.NoError(t, err)

	// break lblFoo by deleting it
	require.NoError(t, s.ds.DeleteLabel(ctx, lblFoo.Name))

	// test that all fields are correctly returned with team 2
	var listResp listMDMConfigProfilesResponse
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp, "team_id", fmt.Sprint(tm2.ID))
	require.Len(t, listResp.Profiles, 3)
	require.NotZero(t, listResp.Profiles[0].CreatedAt)
	require.NotZero(t, listResp.Profiles[0].UploadedAt)
	require.NotZero(t, listResp.Profiles[1].CreatedAt)
	require.NotZero(t, listResp.Profiles[1].UploadedAt)
	listResp.Profiles[0].CreatedAt, listResp.Profiles[0].UploadedAt = time.Time{}, time.Time{}
	listResp.Profiles[1].CreatedAt, listResp.Profiles[1].UploadedAt = time.Time{}, time.Time{}
	listResp.Profiles[2].CreatedAt, listResp.Profiles[2].UploadedAt = time.Time{}, time.Time{}
	require.Equal(t, &fleet.MDMConfigProfilePayload{
		ProfileUUID: tm2ProfF.ProfileUUID,
		TeamID:      tm2ProfF.TeamID,
		Name:        tm2ProfF.Name,
		Platform:    "darwin",
		Identifier:  tm2ProfF.Identifier,
		Checksum:    tm2ProfF.Checksum,
		Scope:       string(fleet.PayloadScopeSystem),
		// labels are ordered by name
		LabelsExcludeAny: []fleet.ConfigurationProfileLabel{
			{LabelID: lblBar.ID, LabelName: lblBar.Name},
			{LabelID: 0, LabelName: lblFoo.Name, Broken: true},
		},
	}, listResp.Profiles[0])
	require.Equal(t, &fleet.MDMConfigProfilePayload{
		ProfileUUID: tm2ProfG.ProfileUUID,
		TeamID:      tm2ProfG.TeamID,
		Name:        tm2ProfG.Name,
		Platform:    "windows",
		// labels are ordered by name
		LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
			{LabelID: lblBar.ID, LabelName: lblBar.Name},
			{LabelID: 0, LabelName: lblFoo.Name, Broken: true},
		},
	}, listResp.Profiles[1])
	require.Equal(t, &fleet.MDMConfigProfilePayload{
		ProfileUUID: tm2ProfH.ProfileUUID,
		TeamID:      tm2ProfH.TeamID,
		Name:        tm2ProfH.Name,
		Platform:    "windows",
		// labels are ordered by name
		LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
			{LabelID: lblBar.ID, LabelName: lblBar.Name},
			{LabelID: lblBaz.ID, LabelName: lblBaz.Name},
		},
	}, listResp.Profiles[2])

	// get the specific include-all label-based profile returns the information
	var getProfResp getMDMConfigProfileResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/profiles/"+tm2ProfG.ProfileUUID, nil, http.StatusOK, &getProfResp)
	getProfResp.CreatedAt, getProfResp.UploadedAt = time.Time{}, time.Time{}
	require.Equal(t, &fleet.MDMConfigProfilePayload{
		ProfileUUID: tm2ProfG.ProfileUUID,
		TeamID:      tm2ProfG.TeamID,
		Name:        tm2ProfG.Name,
		Platform:    "windows",
		// labels are ordered by name
		LabelsIncludeAll: []fleet.ConfigurationProfileLabel{
			{LabelID: lblBar.ID, LabelName: lblBar.Name},
			{LabelID: 0, LabelName: lblFoo.Name, Broken: true},
		},
	}, getProfResp.MDMConfigProfilePayload)

	// get the specific exclude-any label-based profile returns the information
	getProfResp = getMDMConfigProfileResponse{}
	s.DoJSON("GET", "/api/latest/fleet/mdm/profiles/"+tm2ProfF.ProfileUUID, nil, http.StatusOK, &getProfResp)
	getProfResp.CreatedAt, getProfResp.UploadedAt = time.Time{}, time.Time{}
	require.Equal(t, &fleet.MDMConfigProfilePayload{
		ProfileUUID: tm2ProfF.ProfileUUID,
		TeamID:      tm2ProfF.TeamID,
		Name:        tm2ProfF.Name,
		Platform:    "darwin",
		Identifier:  tm2ProfF.Identifier,
		Checksum:    tm2ProfF.Checksum,
		Scope:       string(fleet.PayloadScopeSystem),
		// labels are ordered by name
		LabelsExcludeAny: []fleet.ConfigurationProfileLabel{
			{LabelID: lblBar.ID, LabelName: lblBar.Name},
			{LabelID: 0, LabelName: lblFoo.Name, Broken: true},
		},
	}, getProfResp.MDMConfigProfilePayload)

	// get the specific include-any label-based profile returns the information
	getProfResp = getMDMConfigProfileResponse{}
	s.DoJSON("GET", "/api/latest/fleet/mdm/profiles/"+tm2ProfH.ProfileUUID, nil, http.StatusOK, &getProfResp)
	getProfResp.CreatedAt, getProfResp.UploadedAt = time.Time{}, time.Time{}
	require.Equal(t, &fleet.MDMConfigProfilePayload{
		ProfileUUID: tm2ProfH.ProfileUUID,
		TeamID:      tm2ProfH.TeamID,
		Name:        tm2ProfH.Name,
		Platform:    "windows",
		// labels are ordered by name
		LabelsIncludeAny: []fleet.ConfigurationProfileLabel{
			{LabelID: lblBar.ID, LabelName: lblBar.Name},
			{LabelID: lblBaz.ID, LabelName: lblBaz.Name},
		},
	}, getProfResp.MDMConfigProfilePayload)
	// list for a non-existing team returns 404
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusNotFound, &listResp, "team_id", "99999")

	cases := []struct {
		queries   []string // alternate query name and value
		teamID    *uint
		wantNames []string
		wantMeta  *fleet.PaginationMetadata
	}{
		{
			wantNames: []string{"A", "B", "C", "D", "E"},
			wantMeta:  &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false},
		},
		{
			queries:   []string{"per_page", "2"},
			wantNames: []string{"A", "B"},
			wantMeta:  &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false},
		},
		{
			queries:   []string{"per_page", "2", "page", "1"},
			wantNames: []string{"C", "D"},
			wantMeta:  &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true},
		},
		{
			queries:   []string{"per_page", "2", "page", "2"},
			wantNames: []string{"E"},
			wantMeta:  &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true},
		},
		{
			queries:   []string{"per_page", "3"},
			teamID:    &tm1.ID,
			wantNames: []string{"tA", "tB", "tC"},
			wantMeta:  &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false},
		},
		{
			queries:   []string{"per_page", "3", "page", "1"},
			teamID:    &tm1.ID,
			wantNames: []string{"tD", "tE"},
			wantMeta:  &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true},
		},
		{
			queries:   []string{"per_page", "3", "page", "2"},
			teamID:    &tm1.ID,
			wantNames: nil,
			wantMeta:  &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true},
		},
		{
			queries:   []string{"per_page", "3"},
			teamID:    &tm2.ID,
			wantNames: []string{"tF", "tG", "tH"},
			wantMeta:  &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false},
		},
		{
			queries:   []string{"per_page", "2"},
			teamID:    &tm3.ID,
			wantNames: nil,
			wantMeta:  &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false},
		},
	}
	for _, c := range cases {
		t.Run(fmt.Sprintf("%v: %#v", c.teamID, c.queries), func(t *testing.T) {
			var listResp listMDMConfigProfilesResponse
			queryArgs := c.queries
			if c.teamID != nil {
				queryArgs = append(queryArgs, "team_id", fmt.Sprint(*c.teamID))
			}
			s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp, queryArgs...)

			require.Equal(t, len(c.wantNames), len(listResp.Profiles))
			require.Equal(t, c.wantMeta, listResp.Meta)

			var gotNames []string
			if len(listResp.Profiles) > 0 {
				gotNames = make([]string, len(listResp.Profiles))
				for i, p := range listResp.Profiles {
					gotNames[i] = p.Name
					if p.Name == "tG" {
						require.Len(t, p.LabelsIncludeAll, 2)
					} else {
						require.Nil(t, p.LabelsIncludeAll)
					}
					if p.Name == "tF" {
						require.Len(t, p.LabelsExcludeAny, 2)
					} else {
						require.Nil(t, p.LabelsExcludeAny)
					}
					if c.teamID == nil {
						// we set it to 0 for global
						require.NotNil(t, p.TeamID)
						require.Zero(t, *p.TeamID)
					} else {
						require.NotNil(t, p.TeamID)
						require.Equal(t, *c.teamID, *p.TeamID)
					}
					require.NotEmpty(t, p.Platform)
				}
			}
			require.Equal(t, c.wantNames, gotNames)
		})
	}
}

func (s *integrationMDMTestSuite) TestWindowsProfileManagement() {
	t := s.T()
	ctx := context.Background()

	err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
	require.NoError(t, err)

	globalProfiles := []string{
		mysql.InsertWindowsProfileForTest(t, s.ds, 0),
		mysql.InsertWindowsProfileForTest(t, s.ds, 0),
		mysql.InsertWindowsProfileForTest(t, s.ds, 0),
	}

	// create a new team
	tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"})
	require.NoError(t, err)
	teamProfiles := []string{
		mysql.InsertWindowsProfileForTest(t, s.ds, tm.ID),
		mysql.InsertWindowsProfileForTest(t, s.ds, tm.ID),
	}

	// create a non-Windows host
	_, err = s.ds.NewHost(context.Background(), &fleet.Host{
		ID:            1,
		OsqueryHostID: ptr.String("non-windows-host"),
		NodeKey:       ptr.String("non-windows-host"),
		UUID:          uuid.New().String(),
		Hostname:      fmt.Sprintf("%sfoo.local.non.windows", t.Name()),
		Platform:      "darwin",
	})
	require.NoError(t, err)

	// create a Windows host that's not enrolled into MDM
	_, err = s.ds.NewHost(context.Background(), &fleet.Host{
		ID:            2,
		OsqueryHostID: ptr.String("not-mdm-enrolled"),
		NodeKey:       ptr.String("not-mdm-enrolled"),
		UUID:          uuid.New().String(),
		Hostname:      fmt.Sprintf("%sfoo.local.not.enrolled", t.Name()),
		Platform:      "windows",
	})
	require.NoError(t, err)

	verifyHostProfileStatus := func(cmds []fleet.ProtoCmdOperation, wantStatus string) {
		for _, cmd := range cmds {
			var gotProfile struct {
				Status  string `db:"status"`
				Retries int    `db:"retries"`
			}
			mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
				stmt := `
				SELECT COALESCE(status, 'pending') as status, retries
				FROM host_mdm_windows_profiles
				WHERE command_uuid = ?`
				return sqlx.GetContext(context.Background(), q, &gotProfile, stmt, cmd.Cmd.CmdID.Value)
			})

			wantDeliveryStatus := fleet.WindowsResponseToDeliveryStatus(wantStatus)
			if gotProfile.Retries <= servermdm.MaxProfileRetries && wantDeliveryStatus == fleet.MDMDeliveryFailed {
				require.EqualValues(t, "pending", gotProfile.Status, "command_uuid", cmd.Cmd.CmdID.Value)
			} else {
				require.EqualValues(t, wantDeliveryStatus, gotProfile.Status, "command_uuid", cmd.Cmd.CmdID.Value)
			}
		}
	}

	verifyProfiles := func(device *mdmtest.TestWindowsMDMClient, n int, fail bool) {
		mdmResponseStatus := syncml.CmdStatusOK
		if fail {
			mdmResponseStatus = syncml.CmdStatusAtomicFailed
		}
		s.awaitTriggerProfileSchedule(t)
		cmds, err := device.StartManagementSession()
		require.NoError(t, err)
		// 2 Status + n profiles
		require.Len(t, cmds, n+2)

		var atomicCmds []fleet.ProtoCmdOperation
		msgID, err := device.GetCurrentMsgID()
		require.NoError(t, err)
		for _, c := range cmds {
			cmdID := c.Cmd.CmdID
			status := syncml.CmdStatusOK
			if c.Verb == "Atomic" {
				atomicCmds = append(atomicCmds, c)
				status = mdmResponseStatus
				require.NotEmpty(t, c.Cmd.ReplaceCommands)
				for _, rc := range c.Cmd.ReplaceCommands {
					require.NotEmpty(t, rc.CmdID)
				}
			}
			device.AppendResponse(fleet.SyncMLCmd{
				XMLName: xml.Name{Local: fleet.CmdStatus},
				MsgRef:  &msgID,
				CmdRef:  &cmdID.Value,
				Cmd:     ptr.String(c.Verb),
				Data:    &status,
				Items:   nil,
				CmdID:   fleet.CmdID{Value: uuid.NewString()},
			})
		}
		// TODO: verify profile contents as well
		require.Len(t, atomicCmds, n)

		// before we send the response, commands should be "pending"
		verifyHostProfileStatus(atomicCmds, "")

		cmds, err = device.SendResponse()
		require.NoError(t, err)
		// the ack of the message should be the only returned command
		require.Len(t, cmds, 1)

		// verify that we updated status in the db
		verifyHostProfileStatus(atomicCmds, mdmResponseStatus)
	}

	checkHostsProfilesMatch := func(host *fleet.Host, wantUUIDs []string) {
		var gotUUIDs []string
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			stmt := `SELECT profile_uuid FROM host_mdm_windows_profiles WHERE host_uuid = ?`
			return sqlx.SelectContext(context.Background(), q, &gotUUIDs, stmt, host.UUID)
		})
		require.ElementsMatch(t, wantUUIDs, gotUUIDs)
	}

	checkHostDetails := func(t *testing.T, host *fleet.Host, wantProfs []string, wantStatus fleet.MDMDeliveryStatus) {
		var gotHostResp getHostResponse
		s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", host.ID), nil, http.StatusOK, &gotHostResp)
		require.NotNil(t, gotHostResp.Host.MDM.Profiles)
		var gotProfs []string
		require.Len(t, *gotHostResp.Host.MDM.Profiles, len(wantProfs))
		for _, p := range *gotHostResp.Host.MDM.Profiles {
			gotProfs = append(gotProfs, strings.Replace(p.Name, "name-", "", 1))
			require.NotNil(t, p.Status)
			require.Equal(t, wantStatus, *p.Status, "profile", p.Name)
			require.Equal(t, "windows", p.Platform)
			// Fleet reserved profiles (e.g., OS updates) should be screened from the host details response
			require.NotContains(t, servermdm.ListFleetReservedWindowsProfileNames(), p.Name)
		}
		require.ElementsMatch(t, wantProfs, gotProfs)
	}

	checkHostsFilteredByOSSettingsStatus := func(t *testing.T, wantHosts []string, wantStatus fleet.MDMDeliveryStatus, teamID *uint, labels ...*fleet.Label) {
		var teamFilter string
		if teamID != nil {
			teamFilter = fmt.Sprintf("&team_id=%d", *teamID)
		}
		var gotHostsResp listHostsResponse
		s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts?os_settings=%s%s", wantStatus, teamFilter), nil, http.StatusOK, &gotHostsResp)
		require.NotNil(t, gotHostsResp.Hosts)
		var gotHosts []string
		for _, h := range gotHostsResp.Hosts {
			gotHosts = append(gotHosts, h.Hostname)
		}
		require.ElementsMatch(t, wantHosts, gotHosts)

		var countHostsResp countHostsResponse
		s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/count?os_settings=%s%s", wantStatus, teamFilter), nil, http.StatusOK, &countHostsResp)
		require.Equal(t, len(wantHosts), countHostsResp.Count)

		for _, l := range labels {
			gotHostsResp = listHostsResponse{}
			s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/labels/%d/hosts?os_settings=%s%s", l.ID, wantStatus, teamFilter), nil, http.StatusOK, &gotHostsResp)
			require.NotNil(t, gotHostsResp.Hosts)
			gotHosts = []string{}
			for _, h := range gotHostsResp.Hosts {
				gotHosts = append(gotHosts, h.Hostname)
			}
			require.ElementsMatch(t, wantHosts, gotHosts, "label", l.Name)

			countHostsResp = countHostsResponse{}
			s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/count?label_id=%d&os_settings=%s%s", l.ID, wantStatus, teamFilter), nil, http.StatusOK, &countHostsResp)
		}
	}

	getProfileUUID := func(t *testing.T, profName string, teamID *uint) string {
		var profUUID string
		mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
			var globalOrTeamID uint
			if teamID != nil {
				globalOrTeamID = *teamID
			}
			return sqlx.GetContext(ctx, tx, &profUUID, `SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`, globalOrTeamID, profName)
		})
		require.NotNil(t, profUUID)
		return profUUID
	}

	checkHostProfileStatus := func(t *testing.T, hostUUID string, profUUID string, wantStatus fleet.MDMDeliveryStatus) {
		var gotStatus fleet.MDMDeliveryStatus
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			stmt := `SELECT status FROM host_mdm_windows_profiles WHERE host_uuid = ? AND profile_uuid = ?`
			err := sqlx.GetContext(context.Background(), q, &gotStatus, stmt, hostUUID, profUUID)
			return err
		})
		require.Equal(t, wantStatus, gotStatus)
	}

	// Create a host and then enroll to MDM.
	host, mdmDevice := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
	// trigger a profile sync
	verifyProfiles(mdmDevice, 3, false)
	checkHostsProfilesMatch(host, globalProfiles)
	checkHostDetails(t, host, globalProfiles, fleet.MDMDeliveryVerifying)

	// can not resend windows configuration profiles as admin or from device endpoint
	res := s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, globalProfiles[0]), nil, http.StatusBadRequest)
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, fleet.CantResendWindowsProfilesMessage)
	deviceToken := "windows-device-token"
	createDeviceTokenForHost(t, s.ds, host.ID, deviceToken)
	res = s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/configuration_profiles/%s/resend", deviceToken, globalProfiles[0]), nil, http.StatusBadRequest)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, fleet.CantResendWindowsProfilesMessage)

	// create new label that includes host
	label := &fleet.Label{
		Name:  t.Name() + "foo",
		Query: "select * from foo;",
	}
	label, err = s.ds.NewLabel(context.Background(), label)
	require.NoError(t, err)
	require.NoError(t, s.ds.RecordLabelQueryExecutions(ctx, host, map[uint]*bool{label.ID: ptr.Bool(true)}, time.Now(), false))

	// simulate osquery reporting host mdm details (host_mdm.enrolled = 1 is condition for
	// hosts filtering by os settings status and generating mdm profiles summaries)
	require.NoError(t, s.ds.SetOrUpdateMDMData(ctx, host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "", false))
	checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, nil, label)
	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{
		Verifying: 1,
	}, nil)

	// another sync shouldn't return profiles
	verifyProfiles(mdmDevice, 0, false)

	// make fleet add a Windows OS Updates profile
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{"mdm": { "windows_updates": {"deadline_days": 1, "grace_period_days": 1} }}`), http.StatusOK, &acResp)
	osUpdatesProf := getProfileUUID(t, servermdm.FleetWindowsOSUpdatesProfileName, nil)

	// os updates is sent via a profiles commands
	verifyProfiles(mdmDevice, 1, false)
	checkHostsProfilesMatch(host, append(globalProfiles, osUpdatesProf))
	// but is hidden from host details response
	checkHostDetails(t, host, globalProfiles, fleet.MDMDeliveryVerifying)

	// os updates profile status doesn't matter for filtered hosts results or summaries
	checkHostProfileStatus(t, host.UUID, osUpdatesProf, fleet.MDMDeliveryVerifying)
	checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, nil, label)
	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{
		Verifying: 1,
	}, nil)
	// force os updates profile to failed, doesn't impact filtered hosts results or summaries
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, osUpdatesProf)
		return err
	})
	checkHostProfileStatus(t, host.UUID, osUpdatesProf, fleet.MDMDeliveryFailed)
	checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, nil, label)
	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{
		Verifying: 1,
	}, nil)
	// force another profile to failed, does impact filtered hosts results and summaries
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, globalProfiles[0])
		return err
	})
	checkHostProfileStatus(t, host.UUID, globalProfiles[0], fleet.MDMDeliveryFailed)
	checkHostsFilteredByOSSettingsStatus(t, []string{}, fleet.MDMDeliveryVerifying, nil, label)           // expect no hosts
	checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryFailed, nil, label) // expect host
	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{
		Failed:    1,
		Verifying: 0,
	}, nil)

	// add the host to a team
	err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{host.ID}))
	require.NoError(t, err)

	// trigger a profile sync, device gets the team profile
	verifyProfiles(mdmDevice, 2, false)
	checkHostsProfilesMatch(host, teamProfiles)
	checkHostDetails(t, host, teamProfiles, fleet.MDMDeliveryVerifying)

	// set new team profiles (delete + addition)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `DELETE FROM mdm_windows_configuration_profiles WHERE profile_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, teamProfiles[1])
		return err
	})
	teamProfiles = []string{
		teamProfiles[0],
		mysql.InsertWindowsProfileForTest(t, s.ds, tm.ID),
	}

	// trigger a profile sync, device gets the team profile
	verifyProfiles(mdmDevice, 1, false)

	// check that we deleted the old profile in the DB
	checkHostsProfilesMatch(host, teamProfiles)
	checkHostDetails(t, host, teamProfiles, fleet.MDMDeliveryVerifying)

	// another sync shouldn't return profiles
	verifyProfiles(mdmDevice, 0, false)

	// set new team profiles (delete + addition)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `DELETE FROM mdm_windows_configuration_profiles WHERE profile_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, teamProfiles[1])
		return err
	})
	teamProfiles = []string{
		teamProfiles[0],
		mysql.InsertWindowsProfileForTest(t, s.ds, tm.ID),
	}
	// trigger a profile sync, this time fail the delivery
	verifyProfiles(mdmDevice, 1, true)

	// check that we deleted the old profile in the DB
	checkHostsProfilesMatch(host, teamProfiles)

	// a second sync gets the profile again, because of delivery retries.
	// Succeed that one
	verifyProfiles(mdmDevice, 1, false)

	// another sync shouldn't return profiles
	verifyProfiles(mdmDevice, 0, false)

	// make fleet add a Windows OS Updates profile
	tmResp := teamResponse{}
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID), json.RawMessage(`{"mdm": { "windows_updates": {"deadline_days": 1, "grace_period_days": 1} }}`), http.StatusOK, &tmResp)
	osUpdatesProf = getProfileUUID(t, servermdm.FleetWindowsOSUpdatesProfileName, &tm.ID)

	// os updates is sent via a profiles commands
	verifyProfiles(mdmDevice, 1, false)
	checkHostsProfilesMatch(host, append(teamProfiles, osUpdatesProf))
	// but is hidden from host details response
	checkHostDetails(t, host, teamProfiles, fleet.MDMDeliveryVerifying)

	// os updates profile status doesn't matter for filtered hosts results or summaries
	checkHostProfileStatus(t, host.UUID, osUpdatesProf, fleet.MDMDeliveryVerifying)
	checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, &tm.ID, label)
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{
		Verifying: 1,
	}, nil)
	// force os updates profile to failed, doesn't impact filtered hosts results or summaries
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, osUpdatesProf)
		return err
	})
	checkHostProfileStatus(t, host.UUID, osUpdatesProf, fleet.MDMDeliveryFailed)
	checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, &tm.ID, label)
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{
		Verifying: 1,
	}, nil)
	// force another profile to failed, does impact filtered hosts results and summaries
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ?`
		_, err := q.ExecContext(context.Background(), stmt, teamProfiles[0])
		return err
	})
	checkHostProfileStatus(t, host.UUID, teamProfiles[0], fleet.MDMDeliveryFailed)
	checkHostsFilteredByOSSettingsStatus(t, []string{}, fleet.MDMDeliveryVerifying, &tm.ID, label)           // expect no hosts
	checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryFailed, &tm.ID, label) // expect host
	s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{
		Failed:    1,
		Verifying: 0,
	}, nil)

	// add a macOS profile to the team
	mcUUID := "a" + uuid.NewString()
	prof := mcBytesForTest("name-"+mcUUID, "identifier-"+mcUUID, mcUUID)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `INSERT INTO mdm_apple_configuration_profiles (profile_uuid, team_id, name, identifier, mobileconfig, checksum, uploaded_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);`
		_, err := q.ExecContext(context.Background(), stmt, mcUUID, tm.ID, "name-"+mcUUID, "identifier-"+mcUUID, prof, test.MakeTestBytes())
		return err
	})

	// trigger a profile sync, device doesn't get the macOS profile
	verifyProfiles(mdmDevice, 0, false)

	// can't resend a macOS profile to a Windows host
	res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, mcUUID), nil, http.StatusUnprocessableEntity)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Profile is not compatible with host platform")
}

func (s *integrationMDMTestSuite) TestApplyTeamsMDMWindowsProfiles() {
	t := s.T()

	// create a team through the service so it initializes the agent ops
	teamName := t.Name() + "team1"
	team := &fleet.Team{
		Name:        teamName,
		Description: "desc team1",
	}
	var createTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
	require.NotZero(t, createTeamResp.Team.ID)
	team = createTeamResp.Team

	rawTeamSpec := func(mdmValue string) json.RawMessage {
		return json.RawMessage(fmt.Sprintf(`{ "specs": [{ "name": %q, "mdm": %s }] }`, team.Name, mdmValue))
	}

	// set the windows custom settings fields
	var applyResp applyTeamSpecsResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/teams", rawTeamSpec(`
		{
			"windows_settings": {
				"custom_settings": [
					{"path": "foo", "labels": ["baz"]},
					{"path": "bar", "labels_exclude_any": ["x", "y"]}
				]
			}
		}
	`), http.StatusOK, &applyResp)
	require.Len(t, applyResp.TeamIDsByName, 1)

	// check that they are returned by a GET /config
	var teamResp getTeamResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.ElementsMatch(t, []fleet.MDMProfileSpec{
		{Path: "foo", LabelsIncludeAll: []string{"baz"}},
		{Path: "bar", LabelsExcludeAny: []string{"x", "y"}},
	}, teamResp.Team.Config.MDM.WindowsSettings.CustomSettings.Value)

	// patch without specifying the windows custom settings fields and an unrelated
	// field, should not remove them
	applyResp = applyTeamSpecsResponse{}
	s.DoJSON("POST", "/api/latest/fleet/spec/teams", rawTeamSpec(`{ "enable_disk_encryption": true }`), http.StatusOK, &applyResp)
	require.Len(t, applyResp.TeamIDsByName, 1)

	// check that they are returned by a GET /config
	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.ElementsMatch(t, []fleet.MDMProfileSpec{
		{Path: "foo", LabelsIncludeAll: []string{"baz"}},
		{Path: "bar", LabelsExcludeAny: []string{"x", "y"}},
	}, teamResp.Team.Config.MDM.WindowsSettings.CustomSettings.Value)

	// patch with explicitly empty windows custom settings fields, would remove
	// them but this is a dry-run
	applyResp = applyTeamSpecsResponse{}
	s.DoJSON("POST", "/api/latest/fleet/spec/teams", rawTeamSpec(`
		{ "windows_settings": { "custom_settings": null } }
  `), http.StatusOK, &applyResp, "dry_run", "true")
	assert.Equal(t, map[string]uint{team.Name: team.ID}, applyResp.TeamIDsByName)

	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.ElementsMatch(t, []fleet.MDMProfileSpec{
		{Path: "foo", LabelsIncludeAll: []string{"baz"}},
		{Path: "bar", LabelsExcludeAny: []string{"x", "y"}},
	}, teamResp.Team.Config.MDM.WindowsSettings.CustomSettings.Value)

	// patch with explicitly empty windows custom settings fields, removes them
	applyResp = applyTeamSpecsResponse{}
	s.DoJSON("POST", "/api/latest/fleet/spec/teams", rawTeamSpec(`
		{ "windows_settings": { "custom_settings": null } }
  `), http.StatusOK, &applyResp)
	require.Len(t, applyResp.TeamIDsByName, 1)

	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.Empty(t, teamResp.Team.Config.MDM.WindowsSettings.CustomSettings.Value)

	// apply with invalid mix of labels fails
	res := s.Do("POST", "/api/latest/fleet/spec/teams", rawTeamSpec(`
		{
			"windows_settings": {
				"custom_settings": [
					{"path": "foo", "labels": ["a"], "labels_include_all": ["b"]}
				]
			}
		}
	`), http.StatusUnprocessableEntity)
	errMsg := extractServerErrorText(res.Body)
	assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
}

func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
	t := s.T()
	ctx := context.Background()

	// create a new team
	tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"})
	require.NoError(t, err)

	bigString := strings.Repeat("a", 1024*1024+1)

	// Profile is too big
	resp := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{{Contents: []byte(bigString)}}},
		http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(resp.Body), "Validation Failed: maximum configuration profile file size is 1 MB")

	// invalid profile (bad mobileconfig)
	resp = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{
			Name: "Bad mobileconfig", Contents: []byte(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PayloadContent</key>
	<array/>`),
		},
	}}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(resp.Body), "Validation Failed: new MDMAppleConfigProfile: plist: error parsing XML property list: XML syntax error")

	// apply an empty set to no-team
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: nil}, http.StatusNoContent)
	// Nothing changed, so no activity items
	s.lastActivityOfTypeDoesNotMatch(
		fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
		`{"team_id": null, "team_name": null}`,
		0,
	)
	s.lastActivityOfTypeDoesNotMatch(
		fleet.ActivityTypeEditedWindowsProfile{}.ActivityName(),
		`{"team_id": null, "team_name": null}`,
		0,
	)
	s.lastActivityOfTypeDoesNotMatch(
		fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
		`{"team_id": null, "team_name": null}`,
		0,
	)

	// apply to both team id and name
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: nil},
		http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID), "team_name", tm.Name)

	// invalid team name
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: nil},
		http.StatusNotFound, "team_name", uuid.New().String())

	// duplicate PayloadDisplayName
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: "N2", Contents: mobileconfigForTest("N1", "I2")},
		{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
		{Name: "N4", Contents: declarationForTest("D1")},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))

	// profiles with reserved macOS identifiers
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
			{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
			{Name: p, Contents: mobileconfigForTest(p, p)},
			{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
			{Name: "N4", Contents: declarationForTest("D1")},
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: payload identifier %s is not allowed", p))
	}

	// payloads with reserved types
	for p := range mobileconfig.FleetPayloadTypes() {
		if p == mobileconfig.FleetCustomSettingsPayloadType {
			// FileVault options in the custom settings payload are checked in file_vault_options_test.go
			continue
		}
		res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
			{Name: "N1", Contents: mobileconfigForTestWithContent("N1", "I1", "II1", p, "")},
			{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
			{Name: "N4", Contents: declarationForTest("D1")},
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		switch p {
		case mobileconfig.FleetFileVaultPayloadType, mobileconfig.FleetRecoveryKeyEscrowPayloadType:
			assert.Contains(t, errMsg, mobileconfig.DiskEncryptionProfileRestrictionErrMsg)
		default:
			assert.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadType(s): %s", p))
		}
	}

	// payloads with reserved identifiers
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
			{Name: "N1", Contents: mobileconfigForTestWithContent("N1", "I1", p, "random", "")},
			{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
			{Name: "N4", Contents: declarationForTest("D1")},
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadIdentifier(s): %s", p))
	}

	// profiles with forbidden declaration types
	for dt := range fleet.ForbiddenDeclTypes {
		res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
			{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
			{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
			{Name: "N4", Contents: declarationForTestWithType("D1", dt)},
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, "Only configuration declarations that don’t require an asset reference are supported", dt)
	}
	// and one more for the software update declaration
	res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
		{Name: "N4", Contents: declarationForTestWithType("D1", "com.apple.configuration.softwareupdate.enforcement.specific")},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Declaration profile can’t include OS updates settings. To control these settings, go to OS updates.")

	// invalid JSON
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
		{Name: "N4", Contents: []byte(`{"foo":}`)},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "N4 is not a valid macOS, Windows, or Android configuration profile")

	// profiles with reserved Windows location URIs
	// bitlocker
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: syncml.FleetBitLockerTargetLocURI, Contents: syncMLForTest(fmt.Sprintf("%s/Foo", syncml.FleetBitLockerTargetLocURI))},
		{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	assert.Contains(t, errMsg, syncml.DiskEncryptionProfileRestrictionErrMsg)

	// os updates
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: syncml.FleetOSUpdateTargetLocURI, Contents: syncMLForTest(fmt.Sprintf("%s/Foo", syncml.FleetOSUpdateTargetLocURI))},
		{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Custom configuration profiles can't include Windows updates settings. To control these settings, use the mdm.windows_updates option.")

	// invalid windows tag
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N3", Contents: []byte(`<Exec></Exec>`)},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Windows configuration profiles can only have <Replace> or <Add> top level elements.")

	// invalid xml
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N3", Contents: []byte(`foo`)},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Windows configuration profiles can only have <Replace> or <Add> top level elements.")

	// successfully apply windows and macOS a profiles for the team, but it's a dry run
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: "N2", Contents: syncMLForTest("./Foo/Bar")},
		{Name: "N4", Contents: declarationForTest("D1")},
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID), "dry_run", "true")
	s.assertConfigProfilesByIdentifier(&tm.ID, "I1", false)
	s.assertWindowsConfigProfilesByName(&tm.ID, "N1", false)

	// successfully apply for a team and verify activities
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: "N2", Contents: syncMLForTest("./Foo/Bar")},
		{Name: "N4", Contents: declarationForTest("D1")},
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID))
	s.assertConfigProfilesByIdentifier(&tm.ID, "I1", true)
	s.assertWindowsConfigProfilesByName(&tm.ID, "N2", true)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
		0,
	)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeEditedWindowsProfile{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
		0,
	)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
		0,
	)

	// batch-apply profiles with labels
	lbl1, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "L1", Query: "select 1;"})
	require.NoError(t, err)
	lbl2, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "L2", Query: "select 1;"})
	require.NoError(t, err)
	lbl3, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "L3", Query: "select 1;"})
	require.NoError(t, err)

	// attempt with an invalid label name
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1"), Labels: []string{lbl1.Name, "no-such-label"}},
	}}, http.StatusBadRequest)
	msg := extractServerErrorText(res.Body)
	require.Contains(t, msg, "some or all the labels provided don't exist")

	// mix of labels fields
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1"), Labels: []string{lbl1.Name}, LabelsExcludeAny: []string{lbl2.Name}},
	}}, http.StatusUnprocessableEntity)
	msg = extractServerErrorText(res.Body)
	require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)

	// successful batch-set
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1"), Labels: []string{lbl1.Name, lbl2.Name}},
		{Name: "N2", Contents: syncMLForTest("./Foo/Bar"), LabelsIncludeAll: []string{lbl1.Name}},
		{Name: "N4", Contents: declarationForTest("D1"), LabelsExcludeAny: []string{lbl2.Name}},
	}}, http.StatusNoContent)

	// confirm expected results
	var listResp listMDMConfigProfilesResponse
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp)
	require.Len(t, listResp.Profiles, 3)
	require.Equal(t, "N1", listResp.Profiles[0].Name)
	require.Equal(t, "N2", listResp.Profiles[1].Name)
	require.Equal(t, "N4", listResp.Profiles[2].Name)
	require.Equal(t, listResp.Profiles[0].LabelsIncludeAll, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl1.ID, LabelName: lbl1.Name},
		{LabelID: lbl2.ID, LabelName: lbl2.Name},
	})
	require.Nil(t, listResp.Profiles[0].LabelsExcludeAny)
	require.Equal(t, listResp.Profiles[1].LabelsIncludeAll, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl1.ID, LabelName: lbl1.Name},
	})
	require.Nil(t, listResp.Profiles[1].LabelsExcludeAny)
	require.Equal(t, listResp.Profiles[2].LabelsExcludeAny, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl2.ID, LabelName: lbl2.Name},
	})
	require.Nil(t, listResp.Profiles[2].LabelsIncludeAll)

	// successful batch-set that updates some labels
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1"), LabelsExcludeAny: []string{lbl1.Name, lbl3.Name}},
		{Name: "N2", Contents: syncMLForTest("./Foo/Bar"), LabelsIncludeAll: []string{lbl2.Name}},
	}}, http.StatusNoContent)

	listResp = listMDMConfigProfilesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp)
	require.Len(t, listResp.Profiles, 2)
	require.Equal(t, "N1", listResp.Profiles[0].Name)
	require.Equal(t, "N2", listResp.Profiles[1].Name)
	require.Equal(t, listResp.Profiles[0].LabelsExcludeAny, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl1.ID, LabelName: lbl1.Name},
		{LabelID: lbl3.ID, LabelName: lbl3.Name},
	})
	require.Nil(t, listResp.Profiles[0].LabelsIncludeAll)
	require.Equal(t, listResp.Profiles[1].LabelsIncludeAll, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl2.ID, LabelName: lbl2.Name},
	})
	require.Nil(t, listResp.Profiles[1].LabelsExcludeAny)

	// names cannot be duplicated across platforms
	declBytes := json.RawMessage(`{
	"Type": "com.apple.configuration.decl.foo",
	"Identifier": "com.fleet.config.foo",
	"Payload": {
		"ServiceType": "com.apple.bash",
		"DataAssetReference": "com.fleet.asset.bash"
	}}`)
	mcBytes := mobileconfigForTest("N1", "I1")
	winBytes := syncMLForTest("./Foo/Bar")

	for _, p := range []struct {
		payload   []fleet.MDMProfileBatchPayload
		expectErr string
	}{
		{
			payload:   []fleet.MDMProfileBatchPayload{{Name: "N1", Contents: mcBytes}, {Name: "N1", Contents: winBytes}},
			expectErr: "More than one configuration profile have the same name 'N1'",
		},
		{
			payload:   []fleet.MDMProfileBatchPayload{{Name: "N1", Contents: declBytes}, {Name: "N1", Contents: winBytes}},
			expectErr: "More than one configuration profile have the same name 'N1'",
		},
		{
			payload:   []fleet.MDMProfileBatchPayload{{Name: "N1", Contents: mcBytes}, {Name: "N1", Contents: declBytes}},
			expectErr: "More than one configuration profile have the same name 'N1'",
		},
	} {
		// team profiles
		res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: p.payload},
			http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg = extractServerErrorText(res.Body)
		require.Contains(t, errMsg, p.expectErr)
		// no team profiles
		res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: p.payload}, http.StatusUnprocessableEntity)
		errMsg = extractServerErrorText(res.Body)
		require.Contains(t, errMsg, p.expectErr)
	}
}

// This tests the new public API endpoint for batch modifying MDM profiles
func (s *integrationMDMTestSuite) TestBatchModifyMDMProfiles() {
	t := s.T()
	ctx := context.Background()

	// create a new team
	tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"})
	require.NoError(t, err)

	bigString := strings.Repeat("a", 1024*1024+1)

	// Profile is too big
	resp := s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{{Profile: []byte(bigString)}}},
		http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(resp.Body), "Validation Failed: maximum configuration profile file size is 1 MB")

	// apply an empty set to no-team
	s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: nil}, http.StatusNoContent)
	// Nothing changed, so no activity items
	s.lastActivityOfTypeDoesNotMatch(
		fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
		`{"team_id": null, "team_name": null}`,
		0,
	)
	s.lastActivityOfTypeDoesNotMatch(
		fleet.ActivityTypeEditedWindowsProfile{}.ActivityName(),
		`{"team_id": null, "team_name": null}`,
		0,
	)
	s.lastActivityOfTypeDoesNotMatch(
		fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
		`{"team_id": null, "team_name": null}`,
		0,
	)

	// apply to both team id and name
	s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: nil},
		http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID), "team_name", tm.Name)

	// invalid team name
	s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: nil},
		http.StatusNotFound, "team_name", uuid.New().String())

	// duplicate PayloadDisplayName
	s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1")},
		{DisplayName: "N2", Profile: mobileconfigForTest("N1", "I2")},
		{DisplayName: "N3", Profile: syncMLForTest("./Foo/Bar")},
		{DisplayName: "N4", Profile: declarationForTest("D1")},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))

	// profiles with reserved macOS identifiers
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		res := s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
			{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1")},
			{DisplayName: p, Profile: mobileconfigForTest(p, p)},
			{DisplayName: "N3", Profile: syncMLForTest("./Foo/Bar")},
			{DisplayName: "N4", Profile: declarationForTest("D1")},
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: payload identifier %s is not allowed", p))
	}

	// payloads with reserved types
	for p := range mobileconfig.FleetPayloadTypes() {
		if p == mobileconfig.FleetCustomSettingsPayloadType {
			// FileVault options in the custom settings payload are checked in file_vault_options_test.go
			continue
		}
		res := s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
			{DisplayName: "N1", Profile: mobileconfigForTestWithContent("N1", "I1", "II1", p, "")},
			{DisplayName: "N3", Profile: syncMLForTest("./Foo/Bar")},
			{DisplayName: "N4", Profile: declarationForTest("D1")},
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		switch p {
		case mobileconfig.FleetFileVaultPayloadType, mobileconfig.FleetRecoveryKeyEscrowPayloadType:
			assert.Contains(t, errMsg, mobileconfig.DiskEncryptionProfileRestrictionErrMsg)
		default:
			assert.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadType(s): %s", p))
		}
	}

	// payloads with reserved identifiers
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		res := s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
			{DisplayName: "N1", Profile: mobileconfigForTestWithContent("N1", "I1", p, "random", "")},
			{DisplayName: "N3", Profile: syncMLForTest("./Foo/Bar")},
			{DisplayName: "N4", Profile: declarationForTest("D1")},
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadIdentifier(s): %s", p))
	}

	// profiles with forbidden declaration types
	for dt := range fleet.ForbiddenDeclTypes {
		res := s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
			{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1")},
			{DisplayName: "N3", Profile: syncMLForTest("./Foo/Bar")},
			{DisplayName: "N4", Profile: declarationForTestWithType("D1", dt)},
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, "Only configuration declarations that don’t require an asset reference are supported", dt)
	}
	// and one more for the software update declaration
	res := s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1")},
		{DisplayName: "N3", Profile: syncMLForTest("./Foo/Bar")},
		{DisplayName: "N4", Profile: declarationForTestWithType("D1", "com.apple.configuration.softwareupdate.enforcement.specific")},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Declaration profile can’t include OS updates settings. To control these settings, go to OS updates.")

	// invalid JSON
	res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1")},
		{DisplayName: "N3", Profile: syncMLForTest("./Foo/Bar")},
		{DisplayName: "N4", Profile: []byte(`{"foo":}`)},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "N4 is not a valid macOS, Windows, or Android configuration profile.")

	// profiles with reserved Windows location URIs
	// bitlocker
	res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1")},
		{DisplayName: syncml.FleetBitLockerTargetLocURI, Profile: syncMLForTest(fmt.Sprintf("%s/Foo", syncml.FleetBitLockerTargetLocURI))},
		{DisplayName: "N3", Profile: syncMLForTest("./Foo/Bar")},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	assert.Contains(t, errMsg, syncml.DiskEncryptionProfileRestrictionErrMsg)

	// os updates
	res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1")},
		{DisplayName: syncml.FleetOSUpdateTargetLocURI, Profile: syncMLForTest(fmt.Sprintf("%s/Foo", syncml.FleetOSUpdateTargetLocURI))},
		{DisplayName: "N3", Profile: syncMLForTest("./Foo/Bar")},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Custom configuration profiles can't include Windows updates settings. To control these settings, use the mdm.windows_updates option.")

	// invalid windows tag
	res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N3", Profile: []byte(`<Exec></Exec>`)},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Windows configuration profiles can only have <Replace> or <Add> top level elements.")

	// invalid xml
	res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N3", Profile: []byte(`foo`)},
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Windows configuration profiles can only have <Replace> or <Add> top level elements.")

	// successfully apply windows and macOS a profiles for the team, but it's a dry run
	s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1")},
		{DisplayName: "N2", Profile: syncMLForTest("./Foo/Bar")},
		{DisplayName: "N4", Profile: declarationForTest("D1")},
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID), "dry_run", "true")
	s.assertConfigProfilesByIdentifier(&tm.ID, "I1", false)
	s.assertWindowsConfigProfilesByName(&tm.ID, "N1", false)

	// successfully apply for a team and verify activities
	s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1")},
		{DisplayName: "N2", Profile: syncMLForTest("./Foo/Bar")},
		{DisplayName: "N4", Profile: declarationForTest("D1")},
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID))
	s.assertConfigProfilesByIdentifier(&tm.ID, "I1", true)
	s.assertWindowsConfigProfilesByName(&tm.ID, "N2", true)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
		0,
	)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeEditedWindowsProfile{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
		0,
	)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
		0,
	)

	// batch-apply profiles with labels
	lbl1, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "L1", Query: "select 1;"})
	require.NoError(t, err)
	lbl2, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "L2", Query: "select 1;"})
	require.NoError(t, err)
	lbl3, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "L3", Query: "select 1;"})
	require.NoError(t, err)

	// attempt with an invalid label name
	res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1"), LabelsIncludeAll: []string{lbl1.Name, "no-such-label"}},
	}}, http.StatusBadRequest)
	msg := extractServerErrorText(res.Body)
	require.Contains(t, msg, "some or all the labels provided don't exist")

	// mix of labels fields
	res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1"), LabelsIncludeAll: []string{lbl1.Name}, LabelsExcludeAny: []string{lbl2.Name}},
	}}, http.StatusUnprocessableEntity)
	msg = extractServerErrorText(res.Body)
	require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)

	// successful batch-set
	s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1"), LabelsIncludeAny: []string{lbl1.Name, lbl2.Name}},
		{DisplayName: "N2", Profile: syncMLForTest("./Foo/Bar"), LabelsIncludeAll: []string{lbl1.Name}},
		{DisplayName: "N4", Profile: declarationForTest("D1"), LabelsExcludeAny: []string{lbl2.Name}},
	}}, http.StatusNoContent)

	// confirm expected results
	var listResp listMDMConfigProfilesResponse
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp)
	require.Len(t, listResp.Profiles, 3)
	require.Equal(t, "N1", listResp.Profiles[0].Name)
	require.Equal(t, "N2", listResp.Profiles[1].Name)
	require.Equal(t, "N4", listResp.Profiles[2].Name)
	require.Equal(t, listResp.Profiles[0].LabelsIncludeAny, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl1.ID, LabelName: lbl1.Name},
		{LabelID: lbl2.ID, LabelName: lbl2.Name},
	})
	require.Nil(t, listResp.Profiles[0].LabelsExcludeAny)
	require.Equal(t, listResp.Profiles[1].LabelsIncludeAll, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl1.ID, LabelName: lbl1.Name},
	})
	require.Nil(t, listResp.Profiles[1].LabelsExcludeAny)
	require.Equal(t, listResp.Profiles[2].LabelsExcludeAny, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl2.ID, LabelName: lbl2.Name},
	})
	require.Nil(t, listResp.Profiles[2].LabelsIncludeAll)

	// successful batch-set that updates some labels
	s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
		{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1"), LabelsExcludeAny: []string{lbl1.Name, lbl3.Name}},
		{DisplayName: "N2", Profile: syncMLForTest("./Foo/Bar"), LabelsIncludeAll: []string{lbl2.Name}},
	}}, http.StatusNoContent)

	listResp = listMDMConfigProfilesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp)
	require.Len(t, listResp.Profiles, 2)
	require.Equal(t, "N1", listResp.Profiles[0].Name)
	require.Equal(t, "N2", listResp.Profiles[1].Name)
	require.Equal(t, listResp.Profiles[0].LabelsExcludeAny, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl1.ID, LabelName: lbl1.Name},
		{LabelID: lbl3.ID, LabelName: lbl3.Name},
	})
	require.Nil(t, listResp.Profiles[0].LabelsIncludeAll)
	require.Equal(t, listResp.Profiles[1].LabelsIncludeAll, []fleet.ConfigurationProfileLabel{
		{LabelID: lbl2.ID, LabelName: lbl2.Name},
	})
	require.Nil(t, listResp.Profiles[1].LabelsExcludeAny)

	// names cannot be duplicated across platforms
	declBytes := json.RawMessage(`{
	"Type": "com.apple.configuration.decl.foo",
	"Identifier": "com.fleet.config.foo",
	"Payload": {
		"ServiceType": "com.apple.bash",
		"DataAssetReference": "com.fleet.asset.bash"
	}}`)
	mcBytes := mobileconfigForTest("N1", "I1")
	winBytes := syncMLForTest("./Foo/Bar")

	for _, p := range []struct {
		payload   []fleet.BatchModifyMDMConfigProfilePayload
		expectErr string
	}{
		{
			payload:   []fleet.BatchModifyMDMConfigProfilePayload{{DisplayName: "N1", Profile: mcBytes}, {DisplayName: "N1", Profile: winBytes}},
			expectErr: "More than one configuration profile have the same name 'N1'",
		},
		{
			payload:   []fleet.BatchModifyMDMConfigProfilePayload{{DisplayName: "N1", Profile: declBytes}, {DisplayName: "N1", Profile: winBytes}},
			expectErr: "More than one configuration profile have the same name 'N1'",
		},
		{
			payload:   []fleet.BatchModifyMDMConfigProfilePayload{{DisplayName: "N1", Profile: mcBytes}, {DisplayName: "N1", Profile: declBytes}},
			expectErr: "More than one configuration profile have the same name 'N1'",
		},
	} {
		// team profiles
		res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: p.payload},
			http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg = extractServerErrorText(res.Body)
		require.Contains(t, errMsg, p.expectErr)
		// no team profiles
		res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: p.payload}, http.StatusUnprocessableEntity)
		errMsg = extractServerErrorText(res.Body)
		require.Contains(t, errMsg, p.expectErr)
	}
}

func (s *integrationMDMTestSuite) TestBatchSetMDMProfilesBackwardsCompat() {
	t := s.T()
	ctx := context.Background()

	// create a new team
	tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"})
	require.NoError(t, err)

	// apply an empty set to no-team
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": nil}, http.StatusNoContent)
	// Nothing changed, so no activity
	s.lastActivityOfTypeDoesNotMatch(
		fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
		`{"team_id": null, "team_name": null}`,
		0,
	)
	s.lastActivityOfTypeDoesNotMatch(
		fleet.ActivityTypeEditedWindowsProfile{}.ActivityName(),
		`{"team_id": null, "team_name": null}`,
		0,
	)

	// apply to both team id and name
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": nil},
		http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID), "team_name", tm.Name)

	// invalid team name
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": nil},
		http.StatusNotFound, "team_name", uuid.New().String())

	// duplicate PayloadDisplayName
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
		"N1": mobileconfigForTest("N1", "I1"),
		"N2": mobileconfigForTest("N1", "I2"),
		"N3": syncMLForTest("./Foo/Bar"),
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))

	// profiles with reserved macOS identifiers
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
			"N1": mobileconfigForTest("N1", "I1"),
			p:    mobileconfigForTest(p, p),
			"N3": syncMLForTest("./Foo/Bar"),
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: payload identifier %s is not allowed", p))
	}

	// payloads with reserved types
	for p := range mobileconfig.FleetPayloadTypes() {
		if p == mobileconfig.FleetCustomSettingsPayloadType {
			// FileVault options in the custom settings payload are checked in file_vault_options_test.go
			continue
		}
		res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
			"N1": mobileconfigForTestWithContent("N1", "I1", "II1", p, ""),
			"N3": syncMLForTest("./Foo/Bar"),
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		switch p {
		case mobileconfig.FleetFileVaultPayloadType, mobileconfig.FleetRecoveryKeyEscrowPayloadType:
			assert.Contains(t, errMsg, mobileconfig.DiskEncryptionProfileRestrictionErrMsg)
		default:
			assert.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadType(s): %s", p))
		}
	}

	// payloads with reserved identifiers
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
			"N1": mobileconfigForTestWithContent("N1", "I1", p, "random", ""),
			"N3": syncMLForTest("./Foo/Bar"),
		}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
		errMsg := extractServerErrorText(res.Body)
		require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadIdentifier(s): %s", p))
	}

	// profiles with reserved Windows location URIs
	// bitlocker
	res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
		"N1":                              mobileconfigForTest("N1", "I1"),
		syncml.FleetBitLockerTargetLocURI: syncMLForTest(fmt.Sprintf("%s/Foo", syncml.FleetBitLockerTargetLocURI)),
		"N3":                              syncMLForTest("./Foo/Bar"),
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg := extractServerErrorText(res.Body)
	assert.Contains(t, errMsg, syncml.DiskEncryptionProfileRestrictionErrMsg)

	// os updates
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
		"N1":                             mobileconfigForTest("N1", "I1"),
		syncml.FleetOSUpdateTargetLocURI: syncMLForTest(fmt.Sprintf("%s/Foo", syncml.FleetOSUpdateTargetLocURI)),
		"N3":                             syncMLForTest("./Foo/Bar"),
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Custom configuration profiles can't include Windows updates settings. To control these settings, use the mdm.windows_updates option.")

	// invalid windows tag
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
		"N3": []byte(`<Exec></Exec>`),
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Windows configuration profiles can only have <Replace> or <Add> top level elements.")

	// invalid xml
	res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
		"N3": []byte(`foo`),
	}}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID))
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Windows configuration profiles can only have <Replace> or <Add> top level elements.")

	// successfully apply windows and macOS a profiles for the team, but it's a dry run
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
		"N1": mobileconfigForTest("N1", "I1"),
		"N2": syncMLForTest("./Foo/Bar"),
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID), "dry_run", "true")
	s.assertConfigProfilesByIdentifier(&tm.ID, "I1", false)
	s.assertWindowsConfigProfilesByName(&tm.ID, "N1", false)

	// successfully apply for a team and verify activities
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": map[string][]byte{
		"N1": mobileconfigForTest("N1", "I1"),
		"N2": syncMLForTest("./Foo/Bar"),
	}}, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID))
	s.assertConfigProfilesByIdentifier(&tm.ID, "I1", true)
	s.assertWindowsConfigProfilesByName(&tm.ID, "N2", true)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
		0,
	)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeEditedWindowsProfile{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
		0,
	)
}

func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() {
	t := s.T()
	ctx := context.Background()

	checkMacProfs := func(teamID *uint, names ...string) {
		var count int
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			var tid uint
			if teamID != nil {
				tid = *teamID
			}
			return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_apple_configuration_profiles WHERE team_id = ?`, tid)
		})
		require.Equal(t, len(names), count)
		for _, n := range names {
			s.assertMacOSConfigProfilesByName(teamID, n, true)
		}
	}

	checkWinProfs := func(teamID *uint, names ...string) {
		var count int
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			var tid uint
			if teamID != nil {
				tid = *teamID
			}
			return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_windows_configuration_profiles WHERE team_id = ?`, tid)
		})
		for _, n := range names {
			s.assertWindowsConfigProfilesByName(teamID, n, true)
		}
	}

	acResp := appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	require.True(t, acResp.MDM.EnabledAndConfigured)
	require.True(t, acResp.MDM.WindowsEnabledAndConfigured)

	// ensures that the fleetd profile is created
	secrets, err := s.ds.GetEnrollSecrets(ctx, nil)
	require.NoError(t, err)
	if len(secrets) == 0 {
		require.NoError(t, s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}}))
	}
	require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger))

	// turn on disk encryption and os updates
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"enable_disk_encryption": true,
			"windows_updates": {
				"deadline_days": 3,
				"grace_period_days": 1
			},
			"macos_updates": {
				"deadline": "2023-12-31",
				"minimum_version": "13.3.7"
			}
		}
	}`), http.StatusOK, &acResp)
	checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...)
	checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...)

	// batch set only windows profiles doesn't remove the reserved names
	newWinProfile := syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "l1", Data: "d1"}})
	var testProfiles []fleet.MDMProfileBatchPayload
	testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{
		Name:     "n1",
		Contents: newWinProfile,
	})
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
	checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...)
	checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...)

	// batch set windows and mac profiles doesn't remove the reserved names
	newMacProfile := mcBytesForTest("n2", "i2", uuid.NewString())
	testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{
		Name:     "n2",
		Contents: newMacProfile,
	})
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
	checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...)
	checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...)

	// batch set only mac profiles doesn't remove the reserved names
	testProfiles = []fleet.MDMProfileBatchPayload{{
		Name:     "n2",
		Contents: newMacProfile,
	}}
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
	checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...)
	checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...)

	// create a team
	var tmResp teamResponse
	s.DoJSON("POST", "/api/v1/fleet/teams", map[string]string{"Name": t.Name()}, http.StatusOK, &tmResp)

	// edit team mdm config to turn on disk encryption and os updates
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tmResp.Team.ID), modifyTeamRequest{
		TeamPayload: fleet.TeamPayload{
			Name: ptr.String(t.Name()),
			MDM: &fleet.TeamPayloadMDM{
				EnableDiskEncryption: optjson.SetBool(true),
				WindowsUpdates: &fleet.WindowsUpdates{
					DeadlineDays:    optjson.SetInt(4),
					GracePeriodDays: optjson.SetInt(1),
				},
				MacOSUpdates: &fleet.AppleOSUpdateSettings{
					Deadline:       optjson.SetString("2023-12-31"),
					MinimumVersion: optjson.SetString("13.3.8"),
				},
			},
		},
	}, http.StatusOK, &teamResponse{})

	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d", tmResp.Team.ID), nil, http.StatusOK, &tmResp)
	require.True(t, tmResp.Team.Config.MDM.EnableDiskEncryption)
	require.Equal(t, 4, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value)
	require.Equal(t, 1, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value)
	require.Equal(t, "2023-12-31", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value)
	require.Equal(t, "13.3.8", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value)

	require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger))

	checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...)
	checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...)

	// batch set only windows profiles doesn't remove the reserved names
	var testTeamProfiles []fleet.MDMProfileBatchPayload
	testTeamProfiles = append(testTeamProfiles, fleet.MDMProfileBatchPayload{
		Name:     "n1",
		Contents: newWinProfile,
	})
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tmResp.Team.ID))
	checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...)
	checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...)

	// batch set windows and mac profiles doesn't remove the reserved names
	testTeamProfiles = append(testTeamProfiles, fleet.MDMProfileBatchPayload{
		Name:     "n2",
		Contents: newMacProfile,
	})
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tmResp.Team.ID))
	checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...)
	checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...)

	// batch set only mac profiles doesn't remove the reserved names
	testTeamProfiles = []fleet.MDMProfileBatchPayload{{
		Name:     "n2",
		Contents: newMacProfile,
	}}
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tmResp.Team.ID))
	checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...)
	checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...)
}

func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() {
	t := s.T()
	ctx := context.Background()

	testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"})
	require.NoError(t, err)

	teamDelete, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TeamDelete"})
	require.NoError(t, err)

	testProfiles := make(map[string]fleet.MDMAppleConfigProfile)
	generateTestProfile := func(name string, identifier string) {
		i := identifier
		if i == "" {
			i = fmt.Sprintf("%s.SomeIdentifier", name)
		}
		cp := fleet.MDMAppleConfigProfile{
			Name:       name,
			Identifier: i,
		}
		cp.Mobileconfig = mcBytesForTest(cp.Name, cp.Identifier, fmt.Sprintf("%s.UUID", name))
		testProfiles[name] = cp
	}
	setTestProfileID := func(name string, id uint) {
		tp := testProfiles[name]
		tp.ProfileID = id
		testProfiles[name] = tp
	}

	generateNewReq := func(name string, teamID *uint) (*bytes.Buffer, map[string]string) {
		args := map[string][]string{}
		if teamID != nil {
			args["team_id"] = []string{fmt.Sprintf("%d", *teamID)}
		}
		return generateNewProfileMultipartRequest(t, "some_filename", testProfiles[name].Mobileconfig, s.token, args)
	}

	checkGetResponse := func(resp *http.Response, expected fleet.MDMAppleConfigProfile) {
		// check expected headers
		require.Contains(t, resp.Header["Content-Type"], "application/x-apple-aspen-config")
		require.Contains(t, resp.Header["Content-Disposition"], fmt.Sprintf(`attachment;filename="%s_%s.%s"`, time.Now().Format("2006-01-02"), strings.ReplaceAll(expected.Name, " ", "_"), "mobileconfig"))
		// check expected body
		var bb bytes.Buffer
		_, err = io.Copy(&bb, resp.Body)
		require.NoError(t, err)
		require.Equal(t, []byte(expected.Mobileconfig), bb.Bytes())
	}

	checkConfigProfile := func(expected fleet.MDMAppleConfigProfile, actual fleet.MDMAppleConfigProfile) {
		require.Equal(t, expected.Name, actual.Name)
		require.Equal(t, expected.Identifier, actual.Identifier)
	}

	host, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	s.Do("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{
		TeamID:  &teamDelete.ID,
		HostIDs: []uint{host.ID},
	}, http.StatusOK)

	// create new profile (no team)
	generateTestProfile("TestNoTeam", "")
	body, headers := generateNewReq("TestNoTeam", nil)
	newResp := s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusOK, headers)
	var newCP fleet.MDMAppleConfigProfile
	err = json.NewDecoder(newResp.Body).Decode(&newCP)
	require.NoError(t, err)
	require.NotEmpty(t, newCP.ProfileID)
	setTestProfileID("TestNoTeam", newCP.ProfileID)

	// create new profile (with team id)
	generateTestProfile("TestWithTeamID", "")
	body, headers = generateNewReq("TestWithTeamID", &testTeam.ID)
	newResp = s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusOK, headers)
	err = json.NewDecoder(newResp.Body).Decode(&newCP)
	require.NoError(t, err)
	require.NotEmpty(t, newCP.ProfileID)
	setTestProfileID("TestWithTeamID", newCP.ProfileID)

	// Create a profile that we're going to remove immediately
	generateTestProfile("TestImmediateDelete", "")
	body, headers = generateNewReq("TestImmediateDelete", &teamDelete.ID)
	newResp = s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusOK, headers)
	newCP = fleet.MDMAppleConfigProfile{}
	err = json.NewDecoder(newResp.Body).Decode(&newCP)
	require.NoError(t, err)
	require.NotEmpty(t, newCP.ProfileID)
	setTestProfileID("TestImmediateDelete", newCP.ProfileID)

	// check that host_mdm_apple_profiles entry was created
	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.NotNil(t, hostResp.Host.MDM.Profiles)
	require.Len(t, *hostResp.Host.MDM.Profiles, 1)
	require.Equal(t, (*hostResp.Host.MDM.Profiles)[0].Name, "TestImmediateDelete")

	// now delete the profile before it's sent, we should see the host_mdm_apple_profiles entry go
	// away
	deletedCP := testProfiles["TestImmediateDelete"]
	deletePath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
	var deleteResp deleteMDMAppleConfigProfileResponse
	s.DoJSON("DELETE", deletePath, nil, http.StatusOK, &deleteResp)
	// confirm deleted
	var listResp listMDMAppleConfigProfilesResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesRequest{TeamID: teamDelete.ID}, http.StatusOK, &listResp)
	require.Len(t, listResp.ConfigProfiles, 0)
	getPath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
	_ = s.DoRawWithHeaders("GET", getPath, nil, http.StatusNotFound, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)})
	// confirm no host profiles
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.Nil(t, hostResp.Host.MDM.Profiles)

	// list profiles (no team)
	expectedCP := testProfiles["TestNoTeam"]
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", nil, http.StatusOK, &listResp)
	require.Len(t, listResp.ConfigProfiles, 1)
	respCP := listResp.ConfigProfiles[0]
	require.Equal(t, expectedCP.Name, respCP.Name)
	checkConfigProfile(expectedCP, *respCP)
	require.Empty(t, respCP.Mobileconfig) // list profiles endpoint shouldn't include mobileconfig bytes
	require.Empty(t, respCP.TeamID)       // zero means no team

	// list profiles (team 1)
	expectedCP = testProfiles["TestWithTeamID"]
	listResp = listMDMAppleConfigProfilesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesRequest{TeamID: testTeam.ID}, http.StatusOK, &listResp)
	require.Len(t, listResp.ConfigProfiles, 1)
	respCP = listResp.ConfigProfiles[0]
	require.Equal(t, expectedCP.Name, respCP.Name)
	checkConfigProfile(expectedCP, *respCP)
	require.Empty(t, respCP.Mobileconfig)         // list profiles endpoint shouldn't include mobileconfig bytes
	require.Equal(t, testTeam.ID, *respCP.TeamID) // team 1

	// get profile (no team)
	expectedCP = testProfiles["TestNoTeam"]
	getPath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", expectedCP.ProfileID)
	getResp := s.DoRawWithHeaders("GET", getPath, nil, http.StatusOK, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)})
	checkGetResponse(getResp, expectedCP)

	// get profile (team 1)
	expectedCP = testProfiles["TestWithTeamID"]
	getPath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", expectedCP.ProfileID)
	getResp = s.DoRawWithHeaders("GET", getPath, nil, http.StatusOK, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)})
	checkGetResponse(getResp, expectedCP)

	// delete profile (no team)
	deletedCP = testProfiles["TestNoTeam"]
	deletePath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
	s.DoJSON("DELETE", deletePath, nil, http.StatusOK, &deleteResp)
	// confirm deleted
	listResp = listMDMAppleConfigProfilesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesRequest{}, http.StatusOK, &listResp)
	require.Len(t, listResp.ConfigProfiles, 0)
	getPath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
	_ = s.DoRawWithHeaders("GET", getPath, nil, http.StatusNotFound, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)})

	// delete profile (team 1)
	deletedCP = testProfiles["TestWithTeamID"]
	deletePath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
	deleteResp = deleteMDMAppleConfigProfileResponse{}
	s.DoJSON("DELETE", deletePath, nil, http.StatusOK, &deleteResp)
	// confirm deleted
	listResp = listMDMAppleConfigProfilesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesRequest{TeamID: testTeam.ID}, http.StatusOK, &listResp)
	require.Len(t, listResp.ConfigProfiles, 0)
	getPath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
	_ = s.DoRawWithHeaders("GET", getPath, nil, http.StatusNotFound, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)})

	// fail to create new profile (no team), invalid fleet secret
	testProfiles["badSecrets"] = fleet.MDMAppleConfigProfile{
		Name:       "badSecrets",
		Identifier: "badSecrets.One",
		Mobileconfig: mobileconfig.Mobileconfig(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PayloadContent</key>
	<array/>
	<key>PayloadDisplayName</key>
	<string>badSecrets</string>
	<key>PayloadIdentifier</key>
	<string>badSecrets.One</string>
	<key>PayloadType</key>
	<string>Configuration</string>
	<key>PayloadUUID</key>
	<string>$FLEET_SECRET_INVALID.35E2029E-A0C2-4754-B709-4CAAB1B8D3CB</string>
	<key>PayloadVersion</key>
	<integer>1</integer>
</dict>
</plist>
`),
	}

	body, headers = generateNewReq("badSecrets", nil)
	newResp = s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusUnprocessableEntity, headers)
	errMsg := extractServerErrorText(newResp.Body)
	require.Contains(t, errMsg, "$FLEET_SECRET_INVALID")

	// trying to add/delete profiles with identifiers managed by Fleet fails
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		generateTestProfile("TestNoTeam", p)
		body, headers := generateNewReq("TestNoTeam", nil)
		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)

		generateTestProfile("TestWithTeamID", p)
		body, headers = generateNewReq("TestWithTeamID", nil)
		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)
		cp, err := fleet.NewMDMAppleConfigProfile(mobileconfigForTestWithContent("N1", "I1", p, "random", ""), nil)
		require.NoError(t, err)
		testProfiles["WithContent"] = *cp
		body, headers = generateNewReq("WithContent", nil)
		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)
	}

	// trying to add profiles with identifiers managed by Fleet fails
	for p := range mobileconfig.FleetPayloadIdentifiers() {
		generateTestProfile("TestNoTeam", p)
		body, headers := generateNewReq("TestNoTeam", nil)
		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)

		generateTestProfile("TestWithTeamID", p)
		body, headers = generateNewReq("TestWithTeamID", nil)
		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)
		cp, err := fleet.NewMDMAppleConfigProfile(mobileconfigForTestWithContent("N1", "I1", p, "random", ""), nil)
		require.NoError(t, err)
		testProfiles["WithContent"] = *cp
		body, headers = generateNewReq("WithContent", nil)
		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)
	}

	// trying to add profiles with names reserved by Fleet fails
	for name := range servermdm.FleetReservedProfileNames() {
		cp := &fleet.MDMAppleConfigProfile{
			Name:         name,
			Identifier:   "valid.identifier",
			Mobileconfig: mcBytesForTest(name, "valid.identifier", "some-uuid"),
		}
		body, headers := generateNewProfileMultipartRequest(t, "some_filename", cp.Mobileconfig, s.token, nil)
		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)

		body, headers = generateNewProfileMultipartRequest(t, "some_filename", cp.Mobileconfig, s.token, map[string][]string{
			"team_id": {fmt.Sprintf("%d", testTeam.ID)},
		})
		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)

		cp, err := fleet.NewMDMAppleConfigProfile(mobileconfigForTestWithContent(
			"valid outer name",
			"valid.outer.identifier",
			"valid.inner.identifer",
			"some-uuid",
			name,
		), nil)
		require.NoError(t, err)
		body, headers = generateNewProfileMultipartRequest(t, "some_filename", cp.Mobileconfig, s.token, nil)
		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)

		cp.TeamID = &testTeam.ID
		body, headers = generateNewProfileMultipartRequest(t, "some_filename", cp.Mobileconfig, s.token, map[string][]string{
			"team_id": {fmt.Sprintf("%d", testTeam.ID)},
		})

		s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusBadRequest, headers)
	}

	// make fleet add a FileVault profile
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	profile := s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// try to delete the profile
	deletePath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", profile.ProfileID)
	deleteResp = deleteMDMAppleConfigProfileResponse{}
	s.DoJSON("DELETE", deletePath, nil, http.StatusBadRequest, &deleteResp)
}

func (s *integrationMDMTestSuite) TestHostMDMProfilesExcludeLabels() {
	t := s.T()
	s.setSkipWorkerJobs(t)
	ctx := context.Background()

	triggerReconcileProfiles := func() {
		s.awaitTriggerProfileSchedule(t)
		// this will only mark them as "pending", as the response to confirm
		// profile deployment is asynchronous, so we simulate it here by
		// updating any "pending" (not NULL) profiles to "verifying"
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			if _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil {
				return err
			}
			if _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_declarations SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil {
				return err
			}
			if _, err := q.ExecContext(ctx, `UPDATE host_mdm_windows_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil {
				return err
			}
			return nil
		})
	}

	// run the crons immediately, will create the Fleet-controlled profiles that
	// will then be expected to be applied (e.g. com.fleetdm.fleetd.config and
	// com.fleetdm.caroot)
	// first create the no-team enroll secret (required to create the fleet profiles)
	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret",
		applyEnrollSecretSpecRequest{
			Spec: &fleet.EnrollSecretSpec{Secrets: []*fleet.EnrollSecret{{Secret: "super-global-secret"}}},
		}, http.StatusOK, &applyResp)
	s.awaitTriggerProfileSchedule(t)

	// create an Apple and a Windows host
	appleHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	windowsHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	// create a few labels
	labels := make([]*fleet.Label, 5)
	for i := 0; i < len(labels); i++ {
		label, err := s.ds.NewLabel(ctx, &fleet.Label{Name: fmt.Sprintf("label-%d", i), Query: "select 1;"})
		require.NoError(t, err)
		labels[i] = label
	}
	// simulate reporting label results for those hosts
	appleHost.LabelUpdatedAt = time.Now()
	windowsHost.LabelUpdatedAt = time.Now()
	err := s.ds.UpdateHost(ctx, appleHost)
	require.NoError(t, err)
	err = s.ds.UpdateHost(ctx, windowsHost)
	require.NoError(t, err)

	// set an Apple profile and declaration and a Windows profile
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "A1", Contents: mobileconfigForTest("A1", "A1"), LabelsExcludeAny: []string{labels[0].Name, labels[1].Name}},
		{Name: "W2", Contents: syncMLForTest("./Foo/W2"), LabelsExcludeAny: []string{labels[2].Name, labels[3].Name}},
		{Name: "D3", Contents: declarationForTest("D3"), LabelsExcludeAny: []string{labels[4].Name}},
	}}, http.StatusNoContent)

	// hosts are not members of any label yet, so running the cron applies the labels
	s.awaitTriggerProfileSchedule(t)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "A1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	// simulate the reconcile profiles deployment
	triggerReconcileProfiles()
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "A1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// mark some profiles as verified (despite accepting a HostMacOSProfile struct, it supports Windows too)
	err = apple_mdm.VerifyHostMDMProfiles(ctx, s.ds, appleHost, map[string]*fleet.HostMacOSProfile{
		"A1": {Identifier: "A1", DisplayName: "A1", InstallDate: time.Now()},
	})
	require.NoError(t, err)
	err = apple_mdm.VerifyHostMDMProfiles(ctx, s.ds, windowsHost, map[string]*fleet.HostMacOSProfile{
		"W2": {Identifier: "W2", DisplayName: "W2", InstallDate: time.Now()},
	})
	require.NoError(t, err)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "A1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerified},
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerified},
		},
	})

	// make hosts members of labels [1], [2], [3] and [4], meaning that none of the profiles apply anymore
	err = s.ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{
		{labels[1].ID, appleHost.ID},
		{labels[2].ID, appleHost.ID},
		{labels[3].ID, appleHost.ID},
		{labels[4].ID, appleHost.ID},
		{labels[1].ID, windowsHost.ID},
		{labels[2].ID, windowsHost.ID},
		{labels[3].ID, windowsHost.ID},
		{labels[4].ID, windowsHost.ID},
	})
	require.NoError(t, err)

	s.awaitTriggerProfileSchedule(t)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "A1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	// windows profiles go straight to removed without getting deleted on the host
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {},
	})

	// remove membership of labels [2] for Windows, and [4] for Apple, meaning
	// that only D3 will be installed on Apple (as the Windows host is still
	// member of an excluded label)
	err = s.ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{
		{labels[4].ID, appleHost.ID},
		{labels[2].ID, windowsHost.ID},
	})
	require.NoError(t, err)

	s.awaitTriggerProfileSchedule(t)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "A1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {},
	})

	// remove label [3] as an excluded label for the Windows profile, meaning
	// that the host now meets the requirement to install.
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "A1", Contents: mobileconfigForTest("A1", "A1"), LabelsExcludeAny: []string{labels[0].Name, labels[1].Name}},
		{Name: "W2", Contents: syncMLForTest("./Foo/W2"), LabelsExcludeAny: []string{labels[2].Name}},
		{Name: "D3", Contents: declarationForTest("D3"), LabelsExcludeAny: []string{labels[4].Name}},
	}}, http.StatusNoContent)

	s.awaitTriggerProfileSchedule(t)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "A1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	// simulate the reconcile profiles deployment and mark as verified
	triggerReconcileProfiles()
	err = apple_mdm.VerifyHostMDMProfiles(ctx, s.ds, windowsHost, map[string]*fleet.HostMacOSProfile{
		"W2": {Identifier: "W2", DisplayName: "W2", InstallDate: time.Now()},
	})
	require.NoError(t, err)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerified},
		},
	})

	// break the A1 profile by deleting labels [1]
	err = s.ds.DeleteLabel(ctx, labels[1].Name)
	require.NoError(t, err)

	// it doesn't get installed to the Apple host, as it is broken
	triggerReconcileProfiles()
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerified},
		},
	})

	// it also doesn't get installed to a new host not a member of any labels
	appleHost2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	triggerReconcileProfiles()
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		appleHost2: {
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerified},
		},
	})

	// delete labels [2] and [4], breaking D3 and W2, they don't get removed
	// since they are broken
	err = s.ds.DeleteLabel(ctx, labels[2].Name)
	require.NoError(t, err)
	err = s.ds.DeleteLabel(ctx, labels[4].Name)
	require.NoError(t, err)

	triggerReconcileProfiles()
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
		appleHost2: {
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerified},
		},
	})
}

func (s *integrationMDMTestSuite) TestMDMProfilesIncludeAnyLabels() {
	t := s.T()
	ctx := context.Background()

	triggerReconcileProfiles := func() {
		s.awaitTriggerProfileSchedule(t)
		// this will only mark them as "pending", as the response to confirm
		// profile deployment is asynchronous, so we simulate it here by
		// updating any "pending" (not NULL) profiles to "verifying"
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			if _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil {
				return err
			}
			if _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_declarations SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil {
				return err
			}
			if _, err := q.ExecContext(ctx, `UPDATE host_mdm_windows_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending); err != nil {
				return err
			}
			return nil
		})
	}

	// run the crons immediately, will create the Fleet-controlled profiles that
	// will then be expected to be applied (e.g. com.fleetdm.fleetd.config and
	// com.fleetdm.caroot)
	// first create the no-team enroll secret (required to create the fleet profiles)
	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret",
		applyEnrollSecretSpecRequest{
			Spec: &fleet.EnrollSecretSpec{Secrets: []*fleet.EnrollSecret{{Secret: "super-global-secret"}}},
		}, http.StatusOK, &applyResp)
	s.awaitTriggerProfileSchedule(t)

	// create an Apple and a Windows host
	appleHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	windowsHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	// create a few labels, we'll use the first five for "exclude any" profiles and the remaining for "include any"
	labels := make([]*fleet.Label, 10)
	for i := 0; i < len(labels); i++ {
		label, err := s.ds.NewLabel(ctx, &fleet.Label{Name: fmt.Sprintf("label-%d", i), Query: "select 1;"})
		require.NoError(t, err)
		labels[i] = label
	}
	// simulate reporting label results for those hosts
	appleHost.LabelUpdatedAt = time.Now()
	windowsHost.LabelUpdatedAt = time.Now()
	err := s.ds.UpdateHost(ctx, appleHost)
	require.NoError(t, err)
	err = s.ds.UpdateHost(ctx, windowsHost)
	require.NoError(t, err)

	// set up some Apple profiles and declarations and Windows profiles
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "A1", Contents: mobileconfigForTest("A1", "A1"), LabelsIncludeAny: []string{labels[0].Name, labels[1].Name}},
		{Name: "W2", Contents: syncMLForTest("./Foo/W2"), LabelsIncludeAny: []string{labels[2].Name, labels[3].Name}},
		{Name: "D3", Contents: declarationForTest("D3"), LabelsIncludeAny: []string{labels[4].Name}},
	}}, http.StatusNoContent)

	// hosts are not members of any label yet, so running the cron applies no labels
	s.awaitTriggerProfileSchedule(t)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {},
	})

	// make hosts members of labels [1], [2], [3] and [4], meaning that each of the "include any"
	// labels will now match at least one host
	err = s.ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{
		{labels[0].ID, appleHost.ID},
		{labels[1].ID, appleHost.ID},
		{labels[2].ID, appleHost.ID},
		{labels[3].ID, appleHost.ID},
		{labels[4].ID, appleHost.ID},
		{labels[1].ID, windowsHost.ID},
		{labels[2].ID, windowsHost.ID},
		{labels[3].ID, windowsHost.ID},
		{labels[4].ID, windowsHost.ID},
	})
	require.NoError(t, err)

	triggerReconcileProfiles()
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "A1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})

	// remove membership of labels [2] for Windows, and [1] and [4] for Apple, meaning
	// that D3 will be removed on Apple, A1 will remain on Apple because the host is still a member
	// of [0], and W2 will remain on Windows because the host is still a member of [3]
	err = s.ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{
		{labels[1].ID, appleHost.ID},
		{labels[4].ID, appleHost.ID},
		{labels[2].ID, windowsHost.ID},
	})
	require.NoError(t, err)

	s.awaitTriggerProfileSchedule(t)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		appleHost: {
			{Identifier: "A1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "D3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		windowsHost: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
		},
	})
}

func (s *integrationMDMTestSuite) TestOTAProfile() {
	t := s.T()
	ctx := context.Background()

	// Getting profile for non-existent secret it's ok
	s.Do("GET", "/api/latest/fleet/enrollment_profiles/ota", getOTAProfileRequest{}, http.StatusOK, "enroll_secret", "not-real")

	// Create an enroll secret; has some special characters that should be escaped in the profile
	globalEnrollSec := "global_enroll+_/sec"
	escSec := url.QueryEscape(globalEnrollSec)
	s.Do("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: globalEnrollSec}},
		},
	}, http.StatusOK)

	cfg, err := s.ds.AppConfig(ctx)
	require.NoError(t, err)

	t.Run("gets profile with idp uuid included if boyd cookie is set", func(t *testing.T) {
		// Get profile with that enroll secret
		j, err := json.Marshal(getOTAProfileRequest{})
		require.NoError(t, err)

		idpUUID := uuid.New()
		resp := s.DoRawWithHeaders("GET", "/api/latest/fleet/enrollment_profiles/ota", j, http.StatusOK, map[string]string{
			"Cookie": fmt.Sprintf("%s=%s", shared_mdm.BYODIdpCookieName, idpUUID.String()),
		}, "enroll_secret", globalEnrollSec)
		require.NotZero(t, resp.ContentLength)
		require.Contains(t, resp.Header.Get("Content-Disposition"), `attachment;filename="fleet-mdm-enrollment-profile.mobileconfig"`)
		require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
		require.Contains(t, resp.Header.Get("X-Content-Type-Options"), "nosniff")
		defer resp.Body.Close()
		b, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		require.Equal(t, resp.ContentLength, int64(len(b)))
		require.Contains(t, string(b), "com.fleetdm.fleet.mdm.apple.ota")
		require.Contains(t, string(b), fmt.Sprintf("%s/api/v1/fleet/ota_enrollment?enroll_secret=%s&amp;idp_uuid=%s", cfg.ServerSettings.ServerURL, escSec, idpUUID.String()))
		require.Contains(t, string(b), cfg.OrgInfo.OrgName)
	})

	t.Run("does not include idp_uuid in the url if cookie is not set", func(t *testing.T) {
		resp := s.Do("GET", "/api/latest/fleet/enrollment_profiles/ota", &getOTAProfileRequest{}, http.StatusOK, "enroll_secret", globalEnrollSec)
		require.NotZero(t, resp.ContentLength)
		require.Contains(t, resp.Header.Get("Content-Disposition"), `attachment;filename="fleet-mdm-enrollment-profile.mobileconfig"`)
		require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
		require.Contains(t, resp.Header.Get("X-Content-Type-Options"), "nosniff")
		b, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		require.Equal(t, resp.ContentLength, int64(len(b)))
		require.Contains(t, string(b), "com.fleetdm.fleet.mdm.apple.ota")
		require.Contains(t, string(b), fmt.Sprintf("%s/api/v1/fleet/ota_enrollment?enroll_secret=%s", cfg.ServerSettings.ServerURL, escSec))
		require.NotContains(t, string(b), "idp_uuid=")
		require.Contains(t, string(b), cfg.OrgInfo.OrgName)
	})
}

// TestAppleDDMSecretVariablesUpload tests uploading DDM profiles with secrets via the /configuration_profiles endpoint
func (s *integrationMDMTestSuite) TestAppleDDMSecretVariablesUpload() {
	tmpl := `
{
	"Type": "com.apple.configuration.decl%d",
	"Identifier": "com.fleet.config%d",
	"Payload": {
		"ServiceType": "com.apple.bash%d",
		"DataAssetReference": "com.fleet.asset.bash"
	}
}`

	newProfileBytes := func(i int) []byte {
		return []byte(fmt.Sprintf(tmpl, i, i, i))
	}

	getProfileContents := func(profileUUID string) string {
		profile, err := s.ds.GetMDMAppleDeclaration(context.Background(), profileUUID)
		require.NoError(s.T(), err)
		assert.NotNil(s.T(), profile.SecretsUpdatedAt)
		return string(profile.RawJSON)
	}

	s.testSecretVariablesUpload(newProfileBytes, getProfileContents, "json", "darwin")
}

func (s *integrationMDMTestSuite) testSecretVariablesUpload(newProfileBytes func(i int) []byte,
	getProfileContents func(profileUUID string) string, fileExtension string, platform string,
) {
	t := s.T()
	const numProfiles = 2
	var profiles [][]byte
	for i := 0; i < numProfiles; i++ {
		profiles = append(profiles, newProfileBytes(i))
	}
	// Use secrets
	myBash := "com.apple.bash0"
	profiles[0] = []byte(strings.ReplaceAll(string(profiles[0]), myBash, "$"+fleet.ServerSecretPrefix+"BASH"))
	secretProfile := profiles[1]
	profiles[1] = []byte("${" + fleet.ServerSecretPrefix + "PROFILE}")

	body, headers := generateNewProfileMultipartRequest(
		t, "secret-config0."+fileExtension, profiles[0], s.token, nil,
	)
	res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusUnprocessableEntity, headers)
	assertBodyContains(t, res, `Secret variable \"$FLEET_SECRET_BASH\" missing`)

	// Add secret(s) to server
	req := createSecretVariablesRequest{
		SecretVariables: []fleet.SecretVariable{
			{
				Name:  "FLEET_SECRET_BASH",
				Value: myBash,
			},
			{
				Name:  "FLEET_SECRET_PROFILE",
				Value: string(secretProfile),
			},
		},
	}
	secretResp := createSecretVariablesResponse{}
	s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp)
	res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusOK, headers)
	var resp newMDMConfigProfileResponse
	err := json.NewDecoder(res.Body).Decode(&resp)
	require.NoError(t, err)
	assert.NotEmpty(t, resp.ProfileUUID)

	body, headers = generateNewProfileMultipartRequest(
		t, "secret-config1."+fileExtension, profiles[1], s.token, nil,
	)
	s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp)
	res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusOK, headers)
	err = json.NewDecoder(res.Body).Decode(&resp)
	require.NoError(t, err)
	assert.NotEmpty(t, resp.ProfileUUID)

	var listResp listMDMConfigProfilesResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &listResp)
	require.Len(t, listResp.Profiles, numProfiles)
	profileUUIDs := make([]string, numProfiles)
	for _, p := range listResp.Profiles {
		switch p.Name {
		case "secret-config0":
			assert.Equal(t, platform, p.Platform)
			profileUUIDs[0] = p.ProfileUUID
		case "secret-config1":
			assert.Equal(t, platform, p.Platform)
			profileUUIDs[1] = p.ProfileUUID
		default:
			t.Errorf("unexpected profile %s", p.Name)
		}
	}

	// Check that contents are masking secret values
	for i := 0; i < numProfiles; i++ {
		assert.Equal(t, string(profiles[i]), getProfileContents(profileUUIDs[i]))
	}

	// Delete profiles -- make sure there is no issue deleting profiles with secrets
	for i := 0; i < numProfiles; i++ {
		s.Do("DELETE", "/api/latest/fleet/configuration_profiles/"+profileUUIDs[i], nil, http.StatusOK)
	}
	s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &listResp)
	require.Empty(t, listResp.Profiles)
}

// TestAppleConfigSecretVariablesUpload tests uploading Apple config profiles with secrets via the /configuration_profiles endpoint
func (s *integrationMDMTestSuite) TestAppleConfigSecretVariablesUpload() {
	tmpl := `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>PayloadDescription</key>
  <string>For secret variables</string>
  <key>PayloadDisplayName</key>
  <string>secret-config%d</string>
  <key>PayloadIdentifier</key>
  <string>PI%d</string>
  <key>PayloadType</key>
  <string>Configuration</string>
  <key>PayloadUUID</key>
  <string>%d</string>
  <key>PayloadVersion</key>
  <integer>1</integer>
  <key>PayloadContent</key>
  <array>
    <dict>
      <key>Bash</key>
      <string>$FLEET_SECRET_BASH</string>
      <key>PayloadDisplayName</key>
      <string>secret payload</string>
      <key>PayloadIdentifier</key>
      <string>com.test.secret</string>
      <key>PayloadType</key>
      <string>com.test.secretd</string>
      <key>PayloadUUID</key>
      <string>476F5334-D501-4768-9A31-1A18A4E1E808</string>
      <key>PayloadVersion</key>
      <integer>1</integer>
    </dict>
  </array>
</dict>
</plist>`

	newProfileBytes := func(i int) []byte {
		return []byte(fmt.Sprintf(tmpl, i, i, i))
	}

	getProfileContents := func(profileUUID string) string {
		profile, err := s.ds.GetMDMAppleConfigProfile(context.Background(), profileUUID)
		require.NoError(s.T(), err)
		assert.NotNil(s.T(), profile.SecretsUpdatedAt)
		return string(profile.Mobileconfig)
	}

	s.testSecretVariablesUpload(newProfileBytes, getProfileContents, "mobileconfig", "darwin")
}

// TestWindowsConfigSecretVariablesUpload tests uploading Windows profiles with secrets via the /configuration_profiles endpoint
func (s *integrationMDMTestSuite) TestWindowsConfigSecretVariablesUpload() {
	tmpl := `
<Replace>
    <Item>
        <Meta>
            <Format xmlns="syncml:metinf">int</Format>
        </Meta>
        <Target>
            <LocURI>./Device/Vendor/MSFT/Policy/Config/System/DisableOneDriveFileSync</LocURI>
        </Target>
        <Data>$FLEET_SECRET_BASH</Data>
    </Item>
</Replace>
`

	newProfileBytes := func(i int) []byte {
		return []byte(fmt.Sprintf(tmpl, i, i, i))
	}

	getProfileContents := func(profileUUID string) string {
		profile, err := s.ds.GetMDMWindowsConfigProfile(context.Background(), profileUUID)
		require.NoError(s.T(), err)
		return string(profile.SyncML)
	}

	s.testSecretVariablesUpload(newProfileBytes, getProfileContents, "xml", "windows")
}

func (s *integrationMDMTestSuite) TestAppleProfileDeletion() {
	t := s.T()
	ctx := context.Background()

	err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
	require.NoError(t, err)

	globalProfiles := [][]byte{
		mobileconfigForTest("N1", "I1"),
	}
	wantGlobalProfiles := globalProfiles
	wantGlobalProfiles = append(
		wantGlobalProfiles,
		setupExpectedFleetdProfile(t, s.server.URL, t.Name(), nil),
	)

	// add global profiles
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)

	// Create a host and then enroll to MDM.
	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	// Add IdP email to host
	mysql.ExecAdhocSQL(t, s.ds, func(e sqlx.ExtContext) error {
		_, err := e.ExecContext(ctx, `INSERT INTO host_emails (email, host_id, source) VALUES (?, ?, ?)`, "idp@example.com", host.ID,
			fleet.DeviceMappingMDMIdpAccounts)
		return err
	})

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	installs, removes := checkNextPayloads(t, mdmDevice, false)
	// verify that we received all profiles
	s.signedProfilesMatch(
		append(wantGlobalProfiles, setupExpectedCAProfile(t, s.ds)),
		installs,
	)
	require.Empty(t, removes)

	// Add a profile with a Fleet variable. We are also testing that removal of a profile with a Fleet variable works.
	// A unique command is created for each host when this Fleet variable is used.
	globalProfilesPlusOne := [][]byte{
		globalProfiles[0],
		mobileconfigForTest("N2", "$FLEET_VAR_"+string(fleet.FleetVarHostEndUserEmailIDP)),
	}
	// via the deprecated endpoint, this fails because variables are not supported
	res := s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfilesPlusOne},
		http.StatusUnprocessableEntity)
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "profile variables are not supported by this deprecated endpoint")

	// via the new endpoint, this works
	s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: globalProfilesPlusOne[0]},
		{Name: "N2", Contents: globalProfilesPlusOne[1]},
	}}, http.StatusNoContent)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)

	// Make sure profile was uploaded
	profiles, err := s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	assert.Len(t, profiles, 4)

	// Delete a profile before it is sent to device
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)

	sendErrorOnRemoveProfile := func(device *mdmtest.TestAppleMDMClient) {
		// The host grabs the removal command from Fleet
		cmd, err := device.Idle()
		require.NoError(t, err)
		assert.Equal(t, "RemoveProfile", cmd.Command.RequestType)
		// Since profile is not on the device, it returns an error.
		errChain := []mdm.ErrorChain{
			{
				ErrorCode:            89,
				ErrorDomain:          "FooErrorDomain",
				LocalizedDescription: "The profile not found",
			},
		}
		cmd, err = device.Err(cmd.CommandUUID, errChain)
		require.NoError(t, err)
		assert.Nil(t, cmd)
	}
	sendErrorOnRemoveProfile(mdmDevice)

	// Make sure deleted profile no longer shows up
	profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	assert.Len(t, profiles, 3)

	// Add a profile again
	s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: globalProfilesPlusOne[0]},
		{Name: "N2", Contents: globalProfilesPlusOne[1]},
	}}, http.StatusNoContent)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)

	// The host grabs the profile from Fleet
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	assert.Equal(t, "InstallProfile", cmd.Command.RequestType)
	// Verify that the Fleet variable was replaced with the IdP email
	type Command struct {
		Command struct {
			Payload []byte
		}
	}
	var p Command
	err = plist.Unmarshal(cmd.Raw, &p)
	require.NoError(t, err)
	assert.NotContains(t, string(p.Command.Payload), "$FLEET_VAR_"+fleet.FleetVarHostEndUserEmailIDP)
	assert.Contains(t, string(p.Command.Payload), "idp@example.com")

	// While the host is installing the profile, we delete it.
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)

	// Host acknowledges installing the profile and grabs the remove command
	cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Equal(t, "RemoveProfile", cmd.Command.RequestType)
	cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Nil(t, cmd)

	// Add another device
	host2, mdmDevice2 := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	// Add IdP email to host
	mysql.ExecAdhocSQL(t, s.ds, func(e sqlx.ExtContext) error {
		_, err := e.ExecContext(ctx, `INSERT INTO host_emails (email, host_id, source) VALUES (?, ?, ?)`, "idp2@example.com", host2.ID,
			fleet.DeviceMappingMDMIdpAccounts)
		return err
	})

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	installs, removes = checkNextPayloads(t, mdmDevice2, false)
	assert.Len(t, installs, 3)
	assert.Empty(t, removes)

	// Add a profile again
	s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: globalProfilesPlusOne[0]},
		{Name: "N2", Contents: globalProfilesPlusOne[1]},
	}}, http.StatusNoContent)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	// Delete a profile before it is sent to both devices
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	// The host grabs the removal command from Fleet
	sendErrorOnRemoveProfile(mdmDevice)
	sendErrorOnRemoveProfile(mdmDevice2)

	// Make sure deleted profile no longer shows up on either host
	profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	assert.Len(t, profiles, 3)
	profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host2.UUID)
	require.NoError(t, err)
	assert.Len(t, profiles, 3)
}

func (s *integrationMDMTestSuite) TestBatchResendMDMProfiles() {
	t := s.T()
	ctx := t.Context()
	s.setSkipWorkerJobs(t)

	// create a few hosts
	host1, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	host2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	host3, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	// register a couple profiles for Apple and one for Windows
	profN1 := mobileconfigForTest("N1", "I1")
	profN2 := mobileconfigForTest("N2", "I2")
	profN3 := syncMLForTest("./Foo/N3")
	declN4 := declarationForTest("N4")
	batchRequest := batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: profN1},
		{Name: "N2", Contents: profN2},
		{Name: "N3", Contents: profN3},
		{Name: "N4", Contents: declN4},
	}}
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent)

	// list the profiles to get the UUIDs
	var listResp listMDMConfigProfilesResponse
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp)
	profNameToPayload := make(map[string]*fleet.MDMConfigProfilePayload)
	for _, prof := range listResp.Profiles {
		if len(prof.Checksum) == 0 {
			// not important, but must not be empty or it causes issues when forcing a status
			prof.Checksum = []byte("checksum")
		}
		profNameToPayload[prof.Name] = prof
	}

	// get status for non-existing profile
	s.Do("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", "ano-such-profile"), nil, http.StatusNotFound)
	s.Do("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", "wno-such-profile"), nil, http.StatusNotFound)
	s.Do("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", "dno-such-profile"), nil, http.StatusNotFound)
	s.Do("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", "zno-such-profile"), nil, http.StatusNotFound)

	// get status for existing profiles, all 0 counts
	for _, uuid := range []string{profNameToPayload["N1"].ProfileUUID, profNameToPayload["N2"].ProfileUUID, profNameToPayload["N3"].ProfileUUID} {
		var statusResp getMDMConfigProfileStatusResponse
		s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", uuid), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
		require.Equal(t, fleet.MDMConfigProfileStatus{}, statusResp.MDMConfigProfileStatus)
	}
	// except for the declaration, which is immediately set as pending on the hosts
	var statusResp getMDMConfigProfileStatusResponse
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", profNameToPayload["N4"].ProfileUUID), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
	require.Equal(t, fleet.MDMConfigProfileStatus{Pending: 2}, statusResp.MDMConfigProfileStatus)

	// try to batch-resend a non-existing profile
	batchReq := batchResendMDMProfileToHostsRequest{ProfileUUID: "zzzz"} // not a known prefix
	batchReq.Filters.ProfileStatus = string(fleet.MDMDeliveryFailed)
	s.Do("POST", "/api/v1/fleet/configuration_profiles/resend/batch", batchReq, http.StatusNotFound)
	batchReq = batchResendMDMProfileToHostsRequest{ProfileUUID: "azzzz"} // unknown Apple profile
	batchReq.Filters.ProfileStatus = string(fleet.MDMDeliveryFailed)
	s.Do("POST", "/api/v1/fleet/configuration_profiles/resend/batch", batchReq, http.StatusNotFound)
	batchReq = batchResendMDMProfileToHostsRequest{ProfileUUID: "wzzzz"} // unknown Windows profile
	batchReq.Filters.ProfileStatus = string(fleet.MDMDeliveryFailed)
	s.Do("POST", "/api/v1/fleet/configuration_profiles/resend/batch", batchReq, http.StatusNotFound)

	// batch-resend with an invalid filter
	batchReq = batchResendMDMProfileToHostsRequest{ProfileUUID: profNameToPayload["N1"].ProfileUUID}
	batchReq.Filters.ProfileStatus = string(fleet.MDMDeliveryPending)
	res := s.Do("POST", "/api/v1/fleet/configuration_profiles/resend/batch", batchReq, http.StatusBadRequest)
	msg := extractServerErrorText(res.Body)
	require.Contains(t, msg, "Invalid profile_status filter value, only 'failed' is currently supported.")

	// batch-resend with an Apple DDM
	batchReq = batchResendMDMProfileToHostsRequest{ProfileUUID: profNameToPayload["N4"].ProfileUUID}
	batchReq.Filters.ProfileStatus = string(fleet.MDMDeliveryFailed)
	res = s.Do("POST", "/api/v1/fleet/configuration_profiles/resend/batch", batchReq, http.StatusBadRequest)
	msg = extractServerErrorText(res.Body)
	require.Contains(t, msg, "Can't resend declaration (DDM) profiles.")

	// batch-resend an Apple and a Windows profile, does nothing as it is not delivered yet
	batchReq = batchResendMDMProfileToHostsRequest{ProfileUUID: profNameToPayload["N1"].ProfileUUID}
	batchReq.Filters.ProfileStatus = string(fleet.MDMDeliveryFailed)
	s.Do("POST", "/api/v1/fleet/configuration_profiles/resend/batch", batchReq, http.StatusAccepted)
	batchReq = batchResendMDMProfileToHostsRequest{ProfileUUID: profNameToPayload["N3"].ProfileUUID}
	batchReq.Filters.ProfileStatus = string(fleet.MDMDeliveryFailed)
	s.Do("POST", "/api/v1/fleet/configuration_profiles/resend/batch", batchReq, http.StatusAccepted)

	forceSetAppleHostProfileStatus(t, s.ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N1"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
	forceSetAppleHostProfileStatus(t, s.ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
	forceSetAppleHostProfileStatus(t, s.ds, host2.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N1"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
	forceSetAppleHostProfileStatus(t, s.ds, host2.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
	forceSetWindowsHostProfileStatus(t, s.ds, host3.UUID, test.ToMDMWindowsConfigProfile(profNameToPayload["N3"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: "I1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "I2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "N4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: "I1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "I2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "N4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		host3: {
			{Name: "N3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", profNameToPayload["N1"].ProfileUUID), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
	require.Equal(t, fleet.MDMConfigProfileStatus{Pending: 2}, statusResp.MDMConfigProfileStatus)
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", profNameToPayload["N2"].ProfileUUID), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
	require.Equal(t, fleet.MDMConfigProfileStatus{Pending: 2}, statusResp.MDMConfigProfileStatus)
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", profNameToPayload["N3"].ProfileUUID), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
	require.Equal(t, fleet.MDMConfigProfileStatus{Pending: 1}, statusResp.MDMConfigProfileStatus)
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", profNameToPayload["N4"].ProfileUUID), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
	require.Equal(t, fleet.MDMConfigProfileStatus{Pending: 2}, statusResp.MDMConfigProfileStatus)

	// acknowledge the Apple profiles, failing I2 on both hosts, and fail the Windows one
	forceSetAppleHostProfileStatus(t, s.ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N1"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
	forceSetAppleHostProfileStatus(t, s.ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
	forceSetAppleHostProfileStatus(t, s.ds, host2.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N1"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
	forceSetAppleHostProfileStatus(t, s.ds, host2.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
	forceSetWindowsHostProfileStatus(t, s.ds, host3.UUID, test.ToMDMWindowsConfigProfile(profNameToPayload["N3"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)

	// batch-resend N2 profile
	batchReq = batchResendMDMProfileToHostsRequest{ProfileUUID: profNameToPayload["N2"].ProfileUUID}
	batchReq.Filters.ProfileStatus = string(fleet.MDMDeliveryFailed)
	s.Do("POST", "/api/v1/fleet/configuration_profiles/resend/batch", batchReq, http.StatusAccepted)
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeResentConfigurationProfileBatch{}.ActivityName(),
		fmt.Sprintf(`{"profile_name": %q, "host_count": %d}`, "N2", 2),
		0,
	)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: "I1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "I2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "N4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: "I1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "I2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "N4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		host3: {
			{Name: "N3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryFailed},
		},
	})

	// set I2/N2 as verifying
	forceSetAppleHostProfileStatus(t, s.ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
	forceSetAppleHostProfileStatus(t, s.ds, host2.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["N2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)

	// batch-resend N3 profile
	batchReq = batchResendMDMProfileToHostsRequest{ProfileUUID: profNameToPayload["N3"].ProfileUUID}
	batchReq.Filters.ProfileStatus = string(fleet.MDMDeliveryFailed)
	s.Do("POST", "/api/v1/fleet/configuration_profiles/resend/batch", batchReq, http.StatusAccepted)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: "I1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "I2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "N4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: "I1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "I2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
			{Identifier: "N4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		host3: {
			{Name: "N3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeResentConfigurationProfileBatch{}.ActivityName(),
		fmt.Sprintf(`{"profile_name": %q, "host_count": %d}`, "N3", 1),
		0,
	)

	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", profNameToPayload["N1"].ProfileUUID), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
	require.Equal(t, fleet.MDMConfigProfileStatus{Verifying: 2}, statusResp.MDMConfigProfileStatus)
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", profNameToPayload["N2"].ProfileUUID), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
	require.Equal(t, fleet.MDMConfigProfileStatus{Verifying: 2}, statusResp.MDMConfigProfileStatus)
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", profNameToPayload["N3"].ProfileUUID), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
	require.Equal(t, fleet.MDMConfigProfileStatus{Pending: 1}, statusResp.MDMConfigProfileStatus)
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", profNameToPayload["N4"].ProfileUUID), getMDMConfigProfileStatusRequest{}, http.StatusOK, &statusResp)
	require.Equal(t, fleet.MDMConfigProfileStatus{Pending: 2}, statusResp.MDMConfigProfileStatus)

	// trigger profile schedule to get the fleet-controlled profiles
	s.awaitTriggerProfileSchedule(t)

	// list the profiles to get a fleet-controlled profile UUID
	gotProfs, err := s.ds.GetHostMDMAppleProfiles(ctx, host1.UUID)
	require.NoError(t, err)
	var fleetReservedProfile string
	for _, prof := range gotProfs {
		// find the fleetd config one
		if prof.Identifier == mobileconfig.FleetdConfigPayloadIdentifier {
			fleetReservedProfile = prof.ProfileUUID
		}
	}
	require.NotEmpty(t, fleetReservedProfile)
	// fleet-reserved profiles are not returned by the API, only custom profiles
	s.Do("GET", fmt.Sprintf("/api/v1/fleet/configuration_profiles/%s/status", fleetReservedProfile), getMDMConfigProfileStatusRequest{}, http.StatusNotFound)
}

func (s *integrationMDMTestSuite) TestDeleteMDMProfileCancelsInstalls() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	// create some Apple, Windows and declaration profiles
	profiles := []fleet.MDMProfileBatchPayload{
		{
			Name:     "A1",
			Contents: mobileconfigForTest("A1", "A1"),
		},
		{
			Name:     "A2",
			Contents: mobileconfigForTest("A2", "A2"),
		},
		{
			Name:     "D1",
			Contents: declarationForTest("D1"),
		},
		{
			Name:     "D2",
			Contents: declarationForTest("D2"),
		},
		{
			Name:     "W1",
			Contents: syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "W1", Data: "W1"}}),
		},
		{
			Name:     "W2",
			Contents: syncml.ForTestWithData([]syncml.TestCommand{{Verb: "Replace", LocURI: "W2", Data: "W2"}}),
		},
	}
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: profiles}, http.StatusNoContent)

	// list the profiles to get the UUIDs
	var listResp listMDMConfigProfilesResponse
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp)
	profNameToPayload := make(map[string]*fleet.MDMConfigProfilePayload)
	for _, prof := range listResp.Profiles {
		if len(prof.Checksum) == 0 {
			// not important, but must not be empty or it causes issues when forcing a status
			prof.Checksum = []byte("checksum")
		}
		profNameToPayload[prof.Name] = prof
		t.Logf("profile %s: %s", prof.Name, prof.ProfileUUID)
	}

	// deleting without any affected host is fine
	var deleteResp deleteMDMConfigProfileResponse
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profNameToPayload["A1"].ProfileUUID), nil, http.StatusOK, &deleteResp)
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profNameToPayload["D1"].ProfileUUID), nil, http.StatusOK, &deleteResp)
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profNameToPayload["W1"].ProfileUUID), nil, http.StatusOK, &deleteResp)

	// create some Apple and Windows hosts
	host1, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	host2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	host3, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
	host4, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
	for i, h := range []*fleet.Host{host1, host2, host3, host4} {
		t.Logf("host %d: %s", i+1, h.UUID)
	}
	s.awaitTriggerProfileSchedule(t)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "D2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "D2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		host3: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host4: {
			{Name: "W2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	// for the declaration, set host1 as NULL and host2 as verified
	forceSetAppleHostDeclarationStatus(t, s.ds, host1.UUID, test.ToMDMAppleDecl(profNameToPayload["D2"]), fleet.MDMOperationTypeInstall, "")
	forceSetAppleHostDeclarationStatus(t, s.ds, host2.UUID, test.ToMDMAppleDecl(profNameToPayload["D2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)

	// delete the declaration, will have removed it for host1 and set to remove pending for host2
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profNameToPayload["D2"].ProfileUUID), nil, http.StatusOK, &deleteResp)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "D2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	// for the Windows profile, set host4 as failed
	forceSetWindowsHostProfileStatus(t, s.ds, host4.UUID, test.ToMDMWindowsConfigProfile(profNameToPayload["W2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)

	// delete the Windows profile, will have removed it for both (because there
	// is no "Remove profile" for now with Windows)
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profNameToPayload["W2"].ProfileUUID), nil, http.StatusOK, &deleteResp)

	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		host3: {},
		host4: {},
	})

	// for the Apple profile, set host1 as NULL (pending not queued yet), and leave host2 as actually pending (queued)
	forceSetAppleHostProfileStatus(t, s.ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["A2"]), fleet.MDMOperationTypeInstall, "")

	assertIsCommandActiveForHostAndProfile := func(hostUUID, profileUUID string, wantActive bool) {
		var active bool
		ctx := t.Context()
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			return sqlx.GetContext(ctx, q, &active, `SELECT neq.active
			FROM
				nano_enrollment_queue neq
				JOIN host_mdm_apple_profiles hmap
					ON hmap.command_uuid = neq.command_uuid AND hmap.host_uuid = neq.id
			WHERE
				hmap.host_uuid = ? AND
				hmap.profile_uuid = ?`, hostUUID, profileUUID)
		})
		if wantActive {
			require.True(t, active)
		} else {
			require.False(t, active)
		}
	}

	assertIsCommandActiveForHostAndProfile(host1.UUID, profNameToPayload["A2"].ProfileUUID, true)
	assertIsCommandActiveForHostAndProfile(host2.UUID, profNameToPayload["A2"].ProfileUUID, true)

	// delete the profile, will remove the row for host1 and set host2 to pending remove (and will deactivate the associated nano command)
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profNameToPayload["A2"].ProfileUUID), nil, http.StatusOK, &deleteResp)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: "D2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	assertIsCommandActiveForHostAndProfile(host2.UUID, profNameToPayload["A2"].ProfileUUID, false)

	// set the remove operations to verifying and reconcile profiles
	forceSetAppleHostProfileStatus(t, s.ds, host2.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["A2"]), fleet.MDMOperationTypeRemove, fleet.MDMDeliveryVerifying)
	forceSetAppleHostDeclarationStatus(t, s.ds, host2.UUID, test.ToMDMAppleDecl(profNameToPayload["D2"]), fleet.MDMOperationTypeRemove, fleet.MDMDeliveryVerifying)
	s.awaitTriggerProfileSchedule(t)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})
	s.assertHostWindowsConfigProfiles(map[*fleet.Host][]fleet.HostMDMWindowsProfile{
		host3: {},
		host4: {},
	})

	// add new profile A3 and re-add A2, behaves as if a new profile because it has a new uuid
	oldA2Contents := profiles[1].Contents
	profiles = []fleet.MDMProfileBatchPayload{
		{
			Name:     "A2",
			Contents: oldA2Contents,
		},
		{
			Name:     "A3",
			Contents: mobileconfigForTest("A3", "A3"),
		},
	}
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: profiles}, http.StatusNoContent)

	// list the profiles to get the UUIDs
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp)
	profNameToPayload = make(map[string]*fleet.MDMConfigProfilePayload)
	for _, prof := range listResp.Profiles {
		if len(prof.Checksum) == 0 {
			// not important, but must not be empty or it causes issues when forcing a status
			prof.Checksum = []byte("checksum")
		}
		profNameToPayload[prof.Name] = prof
		t.Logf("new profile %s: %s", prof.Name, prof.ProfileUUID)
	}
	s.awaitTriggerProfileSchedule(t)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "A3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "A3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	// set A3 as failed on host1, and removed on host2
	forceSetAppleHostProfileStatus(t, s.ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["A3"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
	forceSetAppleHostProfileStatus(t, s.ds, host2.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["A3"]), fleet.MDMOperationTypeRemove, fleet.MDMDeliveryFailed)

	// delete the profile, will mark host1 as pending remove and will not touch host2 (not installed)
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", profNameToPayload["A3"].ProfileUUID), nil, http.StatusOK, &deleteResp)

	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "A3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "A3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryFailed},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})

	forceSetAppleHostProfileStatus(t, s.ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToPayload["A3"]), fleet.MDMOperationTypeRemove, fleet.MDMDeliveryVerifying)
	s.awaitTriggerProfileSchedule(t)
	s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
		host1: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
		host2: {
			{Identifier: "A2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: "A3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryFailed},
			{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
			{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
		},
	})
}

// those helper functions to force-set a host profile status are copied from the Datastore
// tests, couldn't put them in the test package due to circular dependency with mysql, would
// be nice to find a way to avoid this copy eventually.
func forceSetAppleHostProfileStatus(t *testing.T, ds *mysql.Datastore, hostUUID string, profile *fleet.MDMAppleConfigProfile, operation fleet.MDMOperationType, status fleet.MDMDeliveryStatus) {
	ctx := t.Context()

	// empty status string means set to NULL
	var actualStatus *fleet.MDMDeliveryStatus
	if status != "" {
		actualStatus = &status
	}

	mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, `INSERT INTO host_mdm_apple_profiles
				(profile_identifier, host_uuid, status, operation_type, command_uuid, profile_name, checksum, profile_uuid)
			VALUES
				(?, ?, ?, ?, ?, ?, UNHEX(MD5(?)), ?)
			ON DUPLICATE KEY UPDATE
				status = VALUES(status),
				operation_type = VALUES(operation_type)
			`,
			profile.Identifier, hostUUID, actualStatus, operation, uuid.NewString(), profile.Name, profile.Mobileconfig, profile.ProfileUUID)
		return err
	})
}

func forceSetWindowsHostProfileStatus(t *testing.T, ds *mysql.Datastore, hostUUID string, profile *fleet.MDMWindowsConfigProfile, operation fleet.MDMOperationType, status fleet.MDMDeliveryStatus) {
	ctx := t.Context()

	// empty status string means set to NULL
	var actualStatus *fleet.MDMDeliveryStatus
	if status != "" {
		actualStatus = &status
	}

	mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, `INSERT INTO host_mdm_windows_profiles
				(host_uuid, status, operation_type, command_uuid, profile_name, checksum, profile_uuid)
			VALUES
				(?, ?, ?, ?, ?, UNHEX(MD5(?)), ?)
			ON DUPLICATE KEY UPDATE
				status = VALUES(status),
				operation_type = VALUES(operation_type)
			`,
			hostUUID, actualStatus, operation, uuid.NewString(), profile.Name, profile.SyncML, profile.ProfileUUID)
		return err
	})
}

func forceSetAppleHostDeclarationStatus(t *testing.T, ds *mysql.Datastore, hostUUID string, profile *fleet.MDMAppleDeclaration, operation fleet.MDMOperationType, status fleet.MDMDeliveryStatus) {
	ctx := t.Context()

	// empty status string means set to NULL
	var actualStatus *fleet.MDMDeliveryStatus
	if status != "" {
		actualStatus = &status
	}

	mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, `INSERT INTO host_mdm_apple_declarations
				(declaration_identifier, host_uuid, status, operation_type, token, declaration_name, declaration_uuid)
			VALUES
				(?, ?, ?, ?, ?, ?, ?)
			ON DUPLICATE KEY UPDATE
				status = VALUES(status),
				operation_type = VALUES(operation_type)
			`,
			profile.Identifier, hostUUID, actualStatus, operation, test.MakeTestBytes(), profile.Name, profile.DeclarationUUID)
		return err
	})
}

func (s *integrationMDMTestSuite) TestVerifyUserScopedProfiles() {
	t := s.T()
	ctx := t.Context()
	s.setSkipWorkerJobs(t)

	// create a macOS host, will enroll only with device
	host, device := createHostThenEnrollMDM(s.ds, s.server.URL, t)

	// create some profiles, system- and user-scoped
	payloadScopeSystem := fleet.PayloadScopeSystem
	payloadScopeUser := fleet.PayloadScopeUser
	profiles := []fleet.MDMProfileBatchPayload{
		{Name: "A1", Contents: scopedMobileconfigForTest("A1", "A1", &payloadScopeSystem)},
		{Name: "A2", Contents: scopedMobileconfigForTest("A2", "A2.user", &payloadScopeUser)},
		{Name: "A3", Contents: scopedMobileconfigForTest("A3", "A3.user", &payloadScopeUser)},
	}
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: profiles}, http.StatusNoContent)

	// ensure we are at least 1s after the profiles uploaded-at timestamp
	time.Sleep(time.Second)

	// list the profiles to get the UUIDs
	var listResp listMDMConfigProfilesResponse
	s.DoJSON("GET", "/api/latest/fleet/configuration_profiles", nil, http.StatusOK, &listResp)
	profNameToPayload := make(map[string]*fleet.MDMConfigProfilePayload)
	for _, prof := range listResp.Profiles {
		if len(prof.Checksum) == 0 {
			// not important, but must not be empty or it causes issues when forcing a status
			prof.Checksum = []byte("checksum")
		}
		profNameToPayload[prof.Name] = prof
		t.Logf("profile %s: %s", prof.Name, prof.ProfileUUID)
	}

	type hostProfile struct {
		ProfileUUID       string  `db:"profile_uuid"`
		ProfileIdentifier string  `db:"profile_identifier"`
		ProfileName       string  `db:"profile_name"`
		Status            *string `db:"status"`
		OperationType     *string `db:"operation_type"`
		Retries           int     `db:"retries"`
		Scope             string  `db:"scope"`
	}
	assertHostProfiles := func(want []hostProfile) {
		var got []hostProfile
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			// for the purpose of this test, we ignore the Fleet-internal profiles
			// (we only care about the custom profiles)
			return sqlx.SelectContext(t.Context(), q, &got, `
				SELECT profile_uuid, profile_identifier, profile_name, status, operation_type, retries, scope
				FROM host_mdm_apple_profiles
				WHERE host_uuid = ? AND profile_identifier NOT IN (?, ?)`,
				host.UUID, mobileconfig.FleetdConfigPayloadIdentifier, mobileconfig.FleetCARootConfigPayloadIdentifier)
		})
		require.ElementsMatch(t, want, got)
	}

	forceProfileUploadeddAtTimestamp := func(ident string, ts time.Time) {
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			_, err := q.ExecContext(ctx, `UPDATE mdm_apple_configuration_profiles
				SET uploaded_at = ? WHERE identifier = ?`, ts, ident)
			return err
		})
	}

	// cron job hasn't run yet, so no profile exist for the host
	assertHostProfiles([]hostProfile{})

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)

	// user-scoped profiles show up as status nil (no user-enrollment yet)
	assertHostProfiles([]hostProfile{
		{
			ProfileUUID:       profNameToPayload["A1"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A1"].Identifier,
			ProfileName:       profNameToPayload["A1"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryPending)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeSystem),
		},
		{
			ProfileUUID:       profNameToPayload["A2"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A2"].Identifier,
			ProfileName:       profNameToPayload["A2"].Name,
			Status:            nil,
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
		{
			ProfileUUID:       profNameToPayload["A3"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A3"].Identifier,
			ProfileName:       profNameToPayload["A3"].Name,
			Status:            nil,
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
	})

	// verify the profiles, only the system one is reported as installed
	host.DetailUpdatedAt = time.Now().UTC()
	err := s.ds.UpdateHost(ctx, host)
	require.NoError(t, err)

	err = apple_mdm.VerifyHostMDMProfiles(ctx, s.ds, host, map[string]*fleet.HostMacOSProfile{
		profNameToPayload["A1"].Identifier: {
			DisplayName: profNameToPayload["A1"].Name,
			Identifier:  profNameToPayload["A1"].Identifier,
			InstallDate: time.Now().UTC(),
		},
	})
	require.NoError(t, err)

	// user-scoped profiles were left untouched
	assertHostProfiles([]hostProfile{
		{
			ProfileUUID:       profNameToPayload["A1"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A1"].Identifier,
			ProfileName:       profNameToPayload["A1"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryVerified)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeSystem),
		},
		{
			ProfileUUID:       profNameToPayload["A2"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A2"].Identifier,
			ProfileName:       profNameToPayload["A2"].Name,
			Status:            nil,
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
		{
			ProfileUUID:       profNameToPayload["A3"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A3"].Identifier,
			ProfileName:       profNameToPayload["A3"].Name,
			Status:            nil,
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
	})

	// create the user-enrollment
	err = device.UserEnroll()
	require.NoError(t, err)

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)

	// user-scoped profiles have been added as pending (not nil)
	assertHostProfiles([]hostProfile{
		{
			ProfileUUID:       profNameToPayload["A1"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A1"].Identifier,
			ProfileName:       profNameToPayload["A1"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryVerified)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeSystem),
		},
		{
			ProfileUUID:       profNameToPayload["A2"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A2"].Identifier,
			ProfileName:       profNameToPayload["A2"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryPending)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
		{
			ProfileUUID:       profNameToPayload["A3"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A3"].Identifier,
			ProfileName:       profNameToPayload["A3"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryPending)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
	})

	// verify the profiles, A3 is missing but still within the grace period
	err = apple_mdm.VerifyHostMDMProfiles(ctx, s.ds, host, map[string]*fleet.HostMacOSProfile{
		profNameToPayload["A1"].Identifier: {
			DisplayName: profNameToPayload["A1"].Name,
			Identifier:  profNameToPayload["A1"].Identifier,
			InstallDate: time.Now().UTC(),
		},
		profNameToPayload["A2"].Identifier: {
			DisplayName: profNameToPayload["A2"].Name,
			Identifier:  profNameToPayload["A2"].Identifier,
			InstallDate: time.Now().UTC(),
		},
	})
	require.NoError(t, err)

	// A2 is now verified, A3 is still pending
	assertHostProfiles([]hostProfile{
		{
			ProfileUUID:       profNameToPayload["A1"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A1"].Identifier,
			ProfileName:       profNameToPayload["A1"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryVerified)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeSystem),
		},
		{
			ProfileUUID:       profNameToPayload["A2"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A2"].Identifier,
			ProfileName:       profNameToPayload["A2"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryVerified)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
		{
			ProfileUUID:       profNameToPayload["A3"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A3"].Identifier,
			ProfileName:       profNameToPayload["A3"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryPending)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
	})

	// rewind the uploaded_at timestamp of A3 so it is not in the grace period
	forceProfileUploadeddAtTimestamp(profNameToPayload["A3"].Identifier, time.Now().Add(-24*time.Hour))

	// report as still missing
	err = apple_mdm.VerifyHostMDMProfiles(ctx, s.ds, host, map[string]*fleet.HostMacOSProfile{
		profNameToPayload["A1"].Identifier: {
			DisplayName: profNameToPayload["A1"].Name,
			Identifier:  profNameToPayload["A1"].Identifier,
			InstallDate: time.Now().UTC(),
		},
		profNameToPayload["A2"].Identifier: {
			DisplayName: profNameToPayload["A2"].Name,
			Identifier:  profNameToPayload["A2"].Identifier,
			InstallDate: time.Now().UTC(),
		},
	})
	require.NoError(t, err)

	// A3 is now missing and retries
	assertHostProfiles([]hostProfile{
		{
			ProfileUUID:       profNameToPayload["A1"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A1"].Identifier,
			ProfileName:       profNameToPayload["A1"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryVerified)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeSystem),
		},
		{
			ProfileUUID:       profNameToPayload["A2"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A2"].Identifier,
			ProfileName:       profNameToPayload["A2"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryVerified)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
		{
			ProfileUUID:       profNameToPayload["A3"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A3"].Identifier,
			ProfileName:       profNameToPayload["A3"].Name,
			Status:            nil,
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           1,
			Scope:             string(fleet.PayloadScopeUser),
		},
	})

	s.awaitTriggerProfileSchedule(t)

	// force-set it to Verifying so that by being missing again it goes to failed
	// (it doesn't go to failed if it is pending)
	forceSetAppleHostProfileStatus(t, s.ds, host.UUID,
		test.ToMDMAppleConfigProfile(profNameToPayload["A3"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)

	err = apple_mdm.VerifyHostMDMProfiles(ctx, s.ds, host, map[string]*fleet.HostMacOSProfile{
		profNameToPayload["A1"].Identifier: {
			DisplayName: profNameToPayload["A1"].Name,
			Identifier:  profNameToPayload["A1"].Identifier,
			InstallDate: time.Now().UTC(),
		},
		profNameToPayload["A2"].Identifier: {
			DisplayName: profNameToPayload["A2"].Name,
			Identifier:  profNameToPayload["A2"].Identifier,
			InstallDate: time.Now().UTC(),
		},
	})
	require.NoError(t, err)

	assertHostProfiles([]hostProfile{
		{
			ProfileUUID:       profNameToPayload["A1"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A1"].Identifier,
			ProfileName:       profNameToPayload["A1"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryVerified)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeSystem),
		},
		{
			ProfileUUID:       profNameToPayload["A2"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A2"].Identifier,
			ProfileName:       profNameToPayload["A2"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryVerified)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           0,
			Scope:             string(fleet.PayloadScopeUser),
		},
		{
			ProfileUUID:       profNameToPayload["A3"].ProfileUUID,
			ProfileIdentifier: profNameToPayload["A3"].Identifier,
			ProfileName:       profNameToPayload["A3"].Name,
			Status:            ptr.String(string(fleet.MDMDeliveryFailed)),
			OperationType:     ptr.String(string(fleet.MDMOperationTypeInstall)),
			Retries:           1,
			Scope:             string(fleet.PayloadScopeUser),
		},
	})
}

func (s *integrationMDMTestSuite) TestMDMAppleProfileScopeChanges() {
	t := s.T()
	ctx := context.Background()

	// add a couple global profiles
	payloadScopeSystem := fleet.PayloadScopeSystem
	payloadScopeUser := fleet.PayloadScopeUser
	globalProfiles := [][]byte{
		mobileconfigForTest("G1", "G1"),
		scopedMobileconfigForTest("G2", "G2", &payloadScopeSystem),
		scopedMobileconfigForTest("G3", "G3.user", &payloadScopeUser),
		scopedMobileconfigForTest("G4", "G4.user-but-actually-system", &payloadScopeUser),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)

	// Create a profile with a scope that is System in the DB but User in the XML. This mimics
	// our upgrade behavior from versions prior to 4.71 to 4.71+ when we added support for User
	// scoped profiles
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := `UPDATE mdm_apple_configuration_profiles SET scope=? WHERE identifier=?;`
		_, err := q.ExecContext(context.Background(), stmt, fleet.PayloadScopeSystem, "G4.user-but-actually-system")
		return err
	})

	// create a team with a couple profiles
	tm1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team_profile_scope_changes_1"})
	require.NoError(t, err)
	tm1Profiles := [][]byte{
		mobileconfigForTest("T1.1", "T1.1"),
		scopedMobileconfigForTest("T1.2", "T1.2", &payloadScopeSystem),
		scopedMobileconfigForTest("T1.3", "T1.3.user", &payloadScopeUser),
	}

	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: tm1Profiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm1.ID))

	// create a second team with different profiles
	tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team_profile_scope_changes_2"})
	require.NoError(t, err)
	tm2Profiles := [][]byte{
		mobileconfigForTest("T2.1", "T2.1"),
		scopedMobileconfigForTest("T2.2", "T2.2.user", &payloadScopeSystem),
		scopedMobileconfigForTest("T2.3", "T2.3.user", &payloadScopeUser),
	}

	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: tm2Profiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm2.ID))

	// Do a no-op update of each team's profiles, verify no errors are returned
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: tm1Profiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm1.ID))
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: tm2Profiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm2.ID))

	// Test a modification of an existing global profile with an implicit scope change
	newGlobalProfiles := [][]byte{
		globalProfiles[0],
		globalProfiles[1],
		globalProfiles[2],
		scopedMobileconfigForTest("G4-bozo", "G4.user-but-actually-system", &payloadScopeUser),
	}
	response := s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: newGlobalProfiles}, http.StatusBadRequest)
	errMsg := extractServerErrorText(response.Body)
	require.Contains(t, errMsg, "Couldn't edit configuration profile (G4.user-but-actually-system) because it was previously delivered to some hosts on the device channel")

	// Test a conflict of a profile on a team with an existing global profile and an implicit scope change
	// Should error because "G4.user-but-actually-system" conflicts with global
	// "G4.user-but-actually-system" profile scope
	newTm1Profiles := [][]byte{
		tm1Profiles[0], // T1.1
		tm1Profiles[1], // T1.2
		tm1Profiles[2], // T1.3.user
		scopedMobileconfigForTest("G4-bozo", "G4.user-but-actually-system", &payloadScopeUser),
	}
	response = s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusBadRequest,
		"team_id", fmt.Sprint(tm1.ID))

	errMsg = extractServerErrorText(response.Body)
	require.Contains(t, errMsg, "Couldn't add configuration profile (G4.user-but-actually-system) because \"PayloadScope\" conflicts")

	// Test a conflict of a profile on a team with an existing global profile
	// Should error because "G2" conflicts with global "G2" profile
	newTm1Profiles = [][]byte{
		tm1Profiles[0], // T1.1
		tm1Profiles[1], // T1.2
		tm1Profiles[2], // T1.3.user
		scopedMobileconfigForTest("G2", "G2", &payloadScopeUser),
	}
	response = s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusBadRequest,
		"team_id", fmt.Sprint(tm1.ID))

	errMsg = extractServerErrorText(response.Body)
	require.Contains(t, errMsg, "Couldn't add configuration profile (G2) because \"PayloadScope\" conflicts")

	// Test a conflict of a profile on a team versus one with the same identifier but different
	// scope on a different team.
	// Should error because "T2.3.user" system-scoped profile conflicts with team2 "T2.3.user" user-scoped profile
	newTm1Profiles = [][]byte{
		tm1Profiles[0], // T1.1
		tm1Profiles[1], // T1.2
		tm1Profiles[2], // T1.3.user
		scopedMobileconfigForTest("T2.3", "T2.3.user", &payloadScopeSystem), // T2.3.user changed to system scope
	}
	response = s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusBadRequest,
		"team_id", fmt.Sprint(tm1.ID))

	errMsg = extractServerErrorText(response.Body)
	require.Contains(t, errMsg, "Couldn't add configuration profile (T2.3.user) because \"PayloadScope\" conflicts")

	// Profile edit of existing profile on team1 with a new scope
	newTm1Profiles = [][]byte{
		tm1Profiles[0], // T1.1
		tm1Profiles[1], // T1.2
		scopedMobileconfigForTest("T1.3", "T1.3.user", &payloadScopeSystem), // T1.3.user changed to system scope
	}
	response = s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusBadRequest,
		"team_id", fmt.Sprint(tm1.ID))

	errMsg = extractServerErrorText(response.Body)
	require.Contains(t, errMsg, "Couldn't edit configuration profile (T1.3.user) because the profile's \"PayloadScope\" has changed")

	// Should be able to add these profiles to team1 with the proper scopes
	newTm1Profiles = [][]byte{
		tm1Profiles[0], // T1.1
		tm1Profiles[1], // T1.2
		tm1Profiles[2], // T1.3.user
		scopedMobileconfigForTest("G2", "G2", &payloadScopeSystem),
		scopedMobileconfigForTest("T2.3", "T2.3.user", &payloadScopeUser), // T2.3.user changed to system scope
	}

	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
		batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusNoContent,
		"team_id", fmt.Sprint(tm1.ID))
}

func (s *integrationMDMTestSuite) TestWindowsProfilesWithFleetVariables() {
	t := s.T()
	ctx := t.Context()

	// Create a team for team-scoped tests
	tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "test_windows_fleet_vars"})
	require.NoError(t, err)

	testCases := []struct {
		name            string
		profiles        []fleet.MDMProfileBatchPayload
		teamID          *uint
		wantStatus      int
		wantErrContains string
	}{
		{
			name: "HOST_UUID variable accepted for team",
			profiles: []fleet.MDMProfileBatchPayload{
				{
					Name: "TestHostUUID",
					Contents: syncml.ForTestWithData([]syncml.TestCommand{
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID", Data: "$FLEET_VAR_HOST_UUID"},
					}),
				},
			},
			teamID:     &tm.ID,
			wantStatus: http.StatusNoContent,
		},
		{
			name: "HOST_UUID variable with braces accepted",
			profiles: []fleet.MDMProfileBatchPayload{
				{
					Name: "TestHostUUIDBraces",
					Contents: syncml.ForTestWithData([]syncml.TestCommand{
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID", Data: "${FLEET_VAR_HOST_UUID}"},
					}),
				},
			},
			teamID:     &tm.ID,
			wantStatus: http.StatusNoContent,
		},
		{
			name: "unsupported variable rejected",
			profiles: []fleet.MDMProfileBatchPayload{
				{
					Name: "TestUnsupported",
					Contents: syncml.ForTestWithData([]syncml.TestCommand{
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/Serial", Data: "$FLEET_VAR_HOST_FAKE"},
					}),
				},
			},
			teamID:          &tm.ID,
			wantStatus:      http.StatusUnprocessableEntity,
			wantErrContains: "Fleet variable $FLEET_VAR_HOST_FAKE is not supported in Windows profiles",
		},
		{
			name: "mixed supported and unsupported variables rejected",
			profiles: []fleet.MDMProfileBatchPayload{
				{
					Name: "TestMixed",
					Contents: syncml.ForTestWithData([]syncml.TestCommand{
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID", Data: "$FLEET_VAR_HOST_UUID"},
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/Email", Data: "$FLEET_VAR_BOGUS"},
					}),
				},
			},
			teamID:          &tm.ID,
			wantStatus:      http.StatusUnprocessableEntity,
			wantErrContains: "Fleet variable $FLEET_VAR_BOGUS is not supported in Windows profiles",
		},
		{
			name: "HOST_UUID variable accepted globally",
			profiles: []fleet.MDMProfileBatchPayload{
				{
					Name: "GlobalHostUUID",
					Contents: syncml.ForTestWithData([]syncml.TestCommand{
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID", Data: "$FLEET_VAR_HOST_UUID"},
					}),
				},
			},
			teamID:     nil, // global profile
			wantStatus: http.StatusNoContent,
		},
		{
			name: "batch with regular and variable profiles accepted",
			profiles: []fleet.MDMProfileBatchPayload{
				{
					Name: "RegularProfile",
					Contents: syncml.ForTestWithData([]syncml.TestCommand{
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/Policy/Config/System/AllowLocation", Data: "1"},
					}),
				},
				{
					Name: "ProfileWithVar",
					Contents: syncml.ForTestWithData([]syncml.TestCommand{
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID", Data: "$FLEET_VAR_HOST_UUID"},
					}),
				},
			},
			teamID:     &tm.ID,
			wantStatus: http.StatusNoContent,
		},
		{
			name: "multiple HOST_UUID variables in single profile accepted",
			profiles: []fleet.MDMProfileBatchPayload{
				{
					Name: "MultipleHostUUID",
					Contents: syncml.ForTestWithData([]syncml.TestCommand{
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID", Data: "$FLEET_VAR_HOST_UUID"},
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/BackupID", Data: "${FLEET_VAR_HOST_UUID}"},
					}),
				},
			},
			teamID:     &tm.ID,
			wantStatus: http.StatusNoContent,
		},
		{
			name: "unknown Fleet variable rejected",
			profiles: []fleet.MDMProfileBatchPayload{
				{
					Name: "UnknownVar",
					Contents: syncml.ForTestWithData([]syncml.TestCommand{
						{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/Policy/Config/System/SomeValue", Data: "${FLEET_VAR_UNKNOWN_VAR}"},
					}),
				},
			},
			teamID:          &tm.ID,
			wantStatus:      http.StatusUnprocessableEntity,
			wantErrContains: "Fleet variable $FLEET_VAR_UNKNOWN_VAR is not supported in Windows profiles",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var resp *http.Response

			// Execute request with or without team_id
			if tc.teamID != nil {
				resp = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch",
					batchSetMDMProfilesRequest{Profiles: tc.profiles},
					tc.wantStatus,
					"team_id", fmt.Sprint(*tc.teamID))
			} else {
				resp = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch",
					batchSetMDMProfilesRequest{Profiles: tc.profiles},
					tc.wantStatus)
			}

			// Check error message if expected
			if tc.wantErrContains != "" {
				errMsg := extractServerErrorText(resp.Body)
				require.Contains(t, errMsg, tc.wantErrContains)
			}
		})
	}
}

func (s *integrationMDMTestSuite) TestWindowsProfilesFleetVariableSubstitution() {
	t := s.T()
	ctx := context.Background()

	// Create a team
	tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "team"})
	require.NoError(t, err)

	// Create and enroll three Windows hosts (two global, one in team)
	hostGlobal1, device1 := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
	hostGlobal2, device2 := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
	hostTeam, deviceTeam := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	// Add the team host to the team
	err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{hostTeam.ID}))
	require.NoError(t, err)

	// Create profiles with HOST_UUID variable for global and team
	globalProfile := syncml.ForTestWithData([]syncml.TestCommand{
		{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID", Data: "Device ID: $FLEET_VAR_HOST_UUID"},
	})
	teamProfile := syncml.ForTestWithData([]syncml.TestCommand{
		{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/TeamDevice/ID", Data: "Team Device: ${FLEET_VAR_HOST_UUID}"},
	})

	// Upload global profile
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch",
		batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
			{Name: "GlobalProfileWithVar", Contents: globalProfile},
		}},
		http.StatusNoContent)

	// Upload team profile
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch",
		batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
			{Name: "TeamProfileWithVar", Contents: teamProfile},
		}},
		http.StatusNoContent,
		"team_id", fmt.Sprint(tm.ID))

	// Helper to verify profile contains substituted UUID
	verifyProfileSubstitution := func(device *mdmtest.TestWindowsMDMClient, expectedUUID string, expectedData string) {
		s.awaitTriggerProfileSchedule(t)
		cmds, err := device.StartManagementSession()
		require.NoError(t, err)

		// Find the Atomic command containing the profile
		var foundProfile bool
		msgID, err := device.GetCurrentMsgID()
		require.NoError(t, err)

		for _, cmd := range cmds {
			// Send status response for each command
			device.AppendResponse(fleet.SyncMLCmd{
				XMLName: xml.Name{Local: fleet.CmdStatus},
				MsgRef:  &msgID,
				CmdRef:  &cmd.Cmd.CmdID.Value,
				Cmd:     ptr.String(cmd.Verb),
				Data:    ptr.String(syncml.CmdStatusOK),
				CmdID:   fleet.CmdID{Value: uuid.NewString()},
			})

			if cmd.Verb == "Atomic" {
				// Check if the command contains our expected data with UUID substituted
				for _, replaceCmd := range cmd.Cmd.ReplaceCommands {
					for _, item := range replaceCmd.Items {
						if item.Data != nil && item.Data.Content != "" {
							if strings.Contains(item.Data.Content, expectedData) {
								// Verify the UUID was substituted correctly
								require.Contains(t, item.Data.Content, expectedUUID)
								require.NotContains(t, item.Data.Content, "$FLEET_VAR_HOST_UUID")
								require.NotContains(t, item.Data.Content, "${FLEET_VAR_HOST_UUID}")
								foundProfile = true
							}
						}
					}
				}
			}
		}
		require.True(t, foundProfile, "Expected profile with UUID substitution not found")

		// Send the response to complete the session
		cmds, err = device.SendResponse()
		require.NoError(t, err)
		// the ack of the message should be the only returned command
		require.Len(t, cmds, 1)
	}

	// Verify global hosts receive profile with their UUID substituted
	verifyProfileSubstitution(device1, hostGlobal1.UUID, "Device ID: "+hostGlobal1.UUID)
	verifyProfileSubstitution(device2, hostGlobal2.UUID, "Device ID: "+hostGlobal2.UUID)

	// Verify team host receives team profile with UUID substituted
	verifyProfileSubstitution(deviceTeam, hostTeam.UUID, "Team Device: "+hostTeam.UUID)

	// Check that profile statuses are updated correctly in the database
	checkHostProfileStatus := func(hostUUID string, profileName string, expectedStatus fleet.MDMDeliveryStatus) {
		profiles, err := s.ds.GetHostMDMWindowsProfiles(ctx, hostUUID)
		require.NoError(t, err)

		// Find the specific profile by name
		var foundProfile *fleet.HostMDMWindowsProfile
		for _, p := range profiles {
			if p.Name == profileName {
				foundProfile = &p
				break
			}
		}
		require.NotNil(t, foundProfile, "Profile %s not found for host %s", profileName, hostUUID)
		require.NotNil(t, foundProfile.Status, "Profile %s status is nil for host %s", profileName, hostUUID)
		assert.Equal(t, expectedStatus, *foundProfile.Status, "Profile %s has unexpected status for host %s", profileName, hostUUID)
	}

	checkHostProfileStatus(hostGlobal1.UUID, "GlobalProfileWithVar", fleet.MDMDeliveryVerifying)
	checkHostProfileStatus(hostGlobal2.UUID, "GlobalProfileWithVar", fleet.MDMDeliveryVerifying)
	checkHostProfileStatus(hostTeam.UUID, "TeamProfileWithVar", fleet.MDMDeliveryVerifying)

	// Now let's check profile verification
	// Also create and test a host without Fleet variables to ensure normal verification still works
	hostNoVars, deviceNoVars := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	// Create a profile without variables
	profileNoVars := syncml.ForTestWithData([]syncml.TestCommand{
		{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/Static/Value", Data: "Static Value: NoSubstitution"},
	})

	// Upload profile without variables
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch",
		batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
			{Name: "ProfileNoVars", Contents: profileNoVars},
		}},
		http.StatusNoContent)

	// Let the host get the profile
	s.awaitTriggerProfileSchedule(t)
	cmds, err := deviceNoVars.StartManagementSession()
	require.NoError(t, err)
	msgID, err := deviceNoVars.GetCurrentMsgID()
	require.NoError(t, err)
	for _, cmd := range cmds {
		deviceNoVars.AppendResponse(fleet.SyncMLCmd{
			XMLName: xml.Name{Local: fleet.CmdStatus},
			MsgRef:  &msgID,
			CmdRef:  &cmd.Cmd.CmdID.Value,
			Cmd:     ptr.String(cmd.Verb),
			Data:    ptr.String(syncml.CmdStatusOK),
			CmdID:   fleet.CmdID{Value: uuid.NewString()},
		})
	}
	cmds, err = deviceNoVars.SendResponse()
	require.NoError(t, err)
	require.Len(t, cmds, 1) // ack

	checkHostProfileStatus(hostNoVars.UUID, "ProfileNoVars", fleet.MDMDeliveryVerifying)

	// To ensure any verification failures result in retry (pending) status instead of staying as verifying,
	// we need to be outside the grace period. The grace period check is:
	// hostDetailUpdatedAt.Before(profileEarliestInstallDate.Add(1 hour))
	//
	// We need to make sure the host checked in recently (detail_updated_at = now)
	// but the profiles are old (created more than 1 hour ago)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		// Set profile timestamps to 2 hours ago
		// IMPORTANT: uploaded_at is used as EarliestInstallDate in verification logic
		_, err := q.ExecContext(ctx,
			`UPDATE mdm_windows_configuration_profiles
			 SET created_at = DATE_SUB(NOW(), INTERVAL 2 HOUR),
			     uploaded_at = DATE_SUB(NOW(), INTERVAL 2 HOUR)
			 WHERE name IN (?, ?, ?)`,
			"GlobalProfileWithVar", "TeamProfileWithVar", "ProfileNoVars")
		if err != nil {
			return err
		}
		// Also update the host profile associations to have old timestamps (only created_at)
		_, err = q.ExecContext(ctx,
			`UPDATE host_mdm_windows_profiles
			 SET created_at = DATE_SUB(NOW(), INTERVAL 2 HOUR)
			 WHERE host_uuid IN (?, ?, ?, ?)`,
			hostGlobal1.UUID, hostGlobal2.UUID, hostTeam.UUID, hostNoVars.UUID)
		if err != nil {
			return err
		}
		// Set host detail_updated_at to now (recent check-in)
		_, err = q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = NOW() WHERE id IN (?, ?, ?, ?)`,
			hostGlobal1.ID, hostGlobal2.ID, hostTeam.ID, hostNoVars.ID)
		return err
	})

	// Helper to simulate osquery reporting back profile data
	simulateOsqueryProfileReport := func(nodeKey string, profileName string, locURI string, reportedData string) {
		// Build a SyncML response that osquery would send back after reading the profile from Windows
		cmdRef := microsoft_mdm.HashLocURI(profileName, locURI)

		var msg fleet.SyncML
		msg.Xmlns = syncml.SyncCmdNamespace
		msg.SyncHdr = fleet.SyncHdr{
			VerDTD:    syncml.SyncMLSupportedVersion,
			VerProto:  syncml.SyncMLVerProto,
			SessionID: "2",
			MsgID:     "2",
		}

		// Add status response (profile was successfully applied)
		msg.AppendCommand(fleet.MDMRaw, fleet.SyncMLCmd{
			XMLName: xml.Name{Local: fleet.CmdStatus},
			CmdID:   fleet.CmdID{Value: uuid.NewString()},
			CmdRef:  &cmdRef,
			Data:    ptr.String("200"),
		})

		// Add results with the data that osquery read from Windows
		msg.AppendCommand(fleet.MDMRaw, fleet.SyncMLCmd{
			XMLName: xml.Name{Local: fleet.CmdResults},
			CmdID:   fleet.CmdID{Value: uuid.NewString()},
			CmdRef:  &cmdRef,
			Items: []fleet.CmdItem{
				{
					Target: ptr.String(locURI),
					Data: &fleet.RawXmlData{
						Content: reportedData,
					},
				},
			},
		})

		rawResponse, err := xml.Marshal(msg)
		require.NoError(t, err)

		// Submit the results via osquery distributed write endpoint
		distributedReq := SubmitDistributedQueryResultsRequest{
			NodeKey: nodeKey,
			Results: map[string][]map[string]string{
				"fleet_detail_query_mdm_config_profiles_windows": {
					{"raw_mdm_command_output": string(rawResponse)},
				},
			},
			Statuses: map[string]fleet.OsqueryStatus{
				"fleet_detail_query_mdm_config_profiles_windows": 0,
			},
		}
		distributedResp := submitDistributedQueryResultsResponse{}
		s.DoJSON("POST", "/api/osquery/distributed/write", distributedReq, http.StatusOK, &distributedResp)
	}

	// First verify that normal profile (without variables) verifies correctly
	simulateOsqueryProfileReport(
		*hostNoVars.NodeKey,
		"ProfileNoVars",
		"./Device/Vendor/MSFT/DMClient/Provider/ProviderID/Static/Value",
		"Static Value: NoSubstitution", // osquery reports exactly what was sent
	)

	// Normal profile should be verified successfully
	checkHostProfileStatus(hostNoVars.UUID, "ProfileNoVars", fleet.MDMDeliveryVerified)

	// Simulate osquery reporting back for team host
	simulateOsqueryProfileReport(
		*hostTeam.NodeKey,
		"TeamProfileWithVar",
		"./Device/Vendor/MSFT/DMClient/Provider/ProviderID/TeamDevice/ID",
		"Team Device: "+hostTeam.UUID, // osquery reports the substituted value
	)

	// Team host has TeamProfileWithVar which now correctly verifies with Fleet variables
	// The fix has been implemented and the profile should be verified successfully
	checkHostProfileStatus(hostTeam.UUID, "TeamProfileWithVar", fleet.MDMDeliveryVerified)

	// Hit the host details API and check the status in the mdm.profiles section
	// Verify team host
	var hostRespTeam getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostTeam.ID), getHostRequest{}, http.StatusOK, &hostRespTeam)
	require.NotNil(t, hostRespTeam.Host.MDM.Profiles)
	require.Len(t, *hostRespTeam.Host.MDM.Profiles, 1)
	require.Equal(t, "TeamProfileWithVar", (*hostRespTeam.Host.MDM.Profiles)[0].Name)
	require.Equal(t, fleet.MDMDeliveryVerified, *(*hostRespTeam.Host.MDM.Profiles)[0].Status,
		"Profile should be verified in host details API for team host")

	// Verify no-vars host
	var hostRespNoVars getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostNoVars.ID), getHostRequest{}, http.StatusOK, &hostRespNoVars)
	require.NotNil(t, hostRespNoVars.Host.MDM.Profiles)
	require.Len(t, *hostRespNoVars.Host.MDM.Profiles, 1)
	require.Equal(t, "ProfileNoVars", (*hostRespNoVars.Host.MDM.Profiles)[0].Name)
	require.Equal(t, fleet.MDMDeliveryVerified, *(*hostRespNoVars.Host.MDM.Profiles)[0].Status,
		"Profile should be verified in host details API for no-vars host")
}

//go:embed testdata/profiles/windows-device-scep.xml
var windowsDeviceSCEPProfileBytes []byte

func (s *integrationMDMTestSuite) TestWindowsDeviceSCEPProfile() {
	testWindowsSCEPProfile(s, windowsDeviceSCEPProfileBytes)
}

//go:embed testdata/profiles/windows-user-scep.xml
var windowsUserSCEPProfileBytes []byte

func (s *integrationMDMTestSuite) TestWindowsUserSCEPProfile() {
	testWindowsSCEPProfile(s, windowsUserSCEPProfileBytes)
}

func testWindowsSCEPProfile(s *integrationMDMTestSuite, windowsScepProfile []byte) {
	t := s.T()
	ctx := context.Background()
	scepServer := scep_server.StartTestSCEPServer(t)
	scepServerURL := scepServer.URL + "/scep"

	// Create windows host and enroll in MDM
	host, mdmDevice := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	verifyCommands := func(wantProfiles int, status string) {
		cmds, err := mdmDevice.StartManagementSession()
		require.NoError(t, err)
		// profile installs + 2 protocol commands acks
		require.Len(t, cmds, wantProfiles+2)
		msgID, err := mdmDevice.GetCurrentMsgID()
		require.NoError(t, err)
		atomicCmds := 0
		for _, c := range cmds {
			if c.Verb == "Atomic" {
				atomicCmds++
			}
			mdmDevice.AppendResponse(fleet.SyncMLCmd{
				XMLName: xml.Name{Local: fleet.CmdStatus},
				MsgRef:  &msgID,
				CmdRef:  ptr.String(c.Cmd.CmdID.Value),
				Cmd:     ptr.String(c.Verb),
				Data:    ptr.String(status),
				Items:   nil,
				CmdID:   fleet.CmdID{Value: uuid.NewString()},
			})
		}
		require.Equal(t, wantProfiles, atomicCmds)
		cmds, err = mdmDevice.SendResponse()
		require.NoError(t, err)
		// the ack of the message should be the only returned command
		require.Len(t, cmds, 1)
	}

	// Upload SCEP profile with missing CA
	resp := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch",
		batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
			{Name: "WindowsSCEPProfile", Contents: windowsScepProfile},
		}},
		http.StatusBadRequest)
	errMsg := extractServerErrorText(resp.Body)
	require.Contains(t, errMsg, "Fleet variable $FLEET_VAR_CUSTOM_SCEP_PROXY_URL_INTEGRATION does not exist.")

	// Create Custom SCEP CA
	ca := &fleet.CertificateAuthority{
		Type:      string(fleet.CATypeCustomSCEPProxy),
		Name:      ptr.String("INTEGRATION"),
		Challenge: ptr.String("integration-test"),
		URL:       ptr.String(scepServerURL),
	}

	_, err := s.ds.NewCertificateAuthority(ctx, ca)
	require.NoError(t, err)

	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch",
		batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
			{Name: "WindowsSCEPProfile", Contents: windowsScepProfile},
		}},
		http.StatusNoContent)

	// Verify host receives the profile
	s.awaitTriggerProfileSchedule(t)

	// Check that profile status is Pending
	profiles, err := s.ds.GetHostMDMWindowsProfiles(ctx, host.UUID)
	require.NoError(t, err)
	var foundProfile bool
	for _, p := range profiles {
		if p.Name == "WindowsSCEPProfile" {
			foundProfile = true
			require.NotNil(t, p.Status)
			assert.Equal(t, fleet.MDMDeliveryPending, *p.Status)
		}
	}
	require.True(t, foundProfile, "WindowsSCEPProfile not found for host")

	verifyCommands(1, syncml.CmdStatusOK)

	// Verify profile status is Verified due to successful response
	profiles, err = s.ds.GetHostMDMWindowsProfiles(ctx, host.UUID)
	require.NoError(t, err)
	foundProfile = false
	for _, p := range profiles {
		if p.Name == "WindowsSCEPProfile" {
			foundProfile = true
			require.NotNil(t, p.Status)
			assert.Equal(t, fleet.MDMDeliveryVerified, *p.Status)
		}
	}
	require.True(t, foundProfile, "WindowsSCEPProfile not found for host")

	// Report Osquery results indicating SCEP profile was applied successfully
	s.reportWindowsOSQueryProfiles(ctx, t, host, map[string][]profileData{
		"WindowsSCEPProfile": {{"200", "L1", "Bogus"}}, // Report back with SCEP LocURI, but data that does not relate SCEP to support the case that we don't verify the success.
	})

	// Verify profile status is still Verified, and OSQuery does not change it's status.
	profiles, err = s.ds.GetHostMDMWindowsProfiles(ctx, host.UUID)
	require.NoError(t, err)
	foundProfile = false
	profileUUID := ""
	for _, p := range profiles {
		if p.Name == "WindowsSCEPProfile" {
			foundProfile = true
			profileUUID = p.ProfileUUID
			require.NotNil(t, p.Status)
			require.Equal(t, fleet.MDMDeliveryVerified, *p.Status)
		}
	}
	require.True(t, foundProfile, "WindowsSCEPProfile not found for host")

	// Attempt simple SCEP call with GetCACaps operation to verify SCEP server is reachable
	identifier := host.UUID + "," + profileUUID + "," + "INTEGRATION"
	scepRes := s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+"/pkiclient.exe", nil, http.StatusOK, nil, "operation", "GetCACaps")
	body, err := io.ReadAll(scepRes.Body)
	require.NoError(t, err)
	assert.Equal(t, scepserver.DefaultCACaps, string(body))
}

// This test verifies that there is no longer a race condition in apple profile resending
func (s *integrationMDMTestSuite) TestAppleProfileResendRaceCondition() {
	t := s.T()
	ctx := context.Background()

	// Create a host and enroll it in MDM
	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setupPusher(s, t, mdmDevice)

	scimUserID, err := s.ds.CreateScimUser(ctx, &fleet.ScimUser{UserName: "user@example.com"})
	require.NoError(t, err)

	// Assign scim user to host
	hostIdStr := fmt.Sprint(host.ID)
	s.Do("PUT", "/api/latest/fleet/hosts/"+hostIdStr+"/device_mapping", putHostDeviceMappingRequest{
		Email:  "user@example.com",
		Source: "idp",
	}, http.StatusOK)

	// Create a profile that uses IDP variables
	profileWithIDPVar := mobileconfigForTestWithContent("TestProfile", "com.test.profile", "com.test.profile.content", "com.test.profile", "Test IDP Variable Profile")

	// Replace the profile content to include an IDP variable
	profileContent := string(profileWithIDPVar)
	profileContent = strings.Replace(profileContent, "<key>ShowRecoveryKey</key>", "<key>TestVariable</key>", 1)
	profileContent = strings.Replace(profileContent, "<false/>", "<string>$FLEET_VAR_HOST_END_USER_IDP_USERNAME</string>", 1)
	profileWithIDPVar = []byte(profileContent)

	// Upload the profile using the new endpoint that supports variables
	s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "TestProfile", Contents: profileWithIDPVar},
	}}, http.StatusNoContent) // Setup SCIM user data for the host

	// No profiles until reconciler
	hostProfiles, err := s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.Empty(t, hostProfiles, "Host should not have any profiles before sync")

	// Trigger initial profile sync - profile should be set to pending/installing
	s.awaitTriggerProfileSchedule(t)

	// Check that install command was sent, but do not acknowledge it yet
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)

	seenProfile := false
	profileCmdID := ""
	for cmd != nil {
		if cmd.Command.RequestType != "InstallProfile" {
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
			continue
		}
		var fullCmd micromdm.CommandPayload
		err = plist.Unmarshal(cmd.Raw, &fullCmd)
		require.NoError(t, err)
		if strings.Contains(string(fullCmd.Command.InstallProfile.Payload), "TestProfile") {
			seenProfile = true
			profileCmdID = cmd.CommandUUID
			break
		}

		// Acknowledge other commands
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}
	require.True(t, seenProfile, "Expected install command for TestProfile not found")

	// Verify profile is in pending status
	hostProfiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)

	var testProfile *fleet.HostMDMAppleProfile
	for _, p := range hostProfiles {
		if p.Identifier == "com.test.profile" {
			testProfile = &p
			break
		}
	}
	require.NotNil(t, testProfile)
	require.Equal(t, fleet.MDMDeliveryPending, *testProfile.Status)

	// Now simulate the race condition:
	// we trigger a resend before the acknowledgement comes back

	// 1. Trigger an IDP variable change by updating SCIM user
	err = s.ds.ReplaceScimUser(ctx, &fleet.ScimUser{ID: scimUserID, UserName: "newuser@example.com"})
	require.NoError(t, err)

	// 2. At this point, the profile should be marked for resend (status = NULL)
	hostProfiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)

	for _, p := range hostProfiles {
		if p.Identifier == "com.test.profile" {
			fmt.Printf("%v\n", p.Status)
			testProfile = &p
			break
		}
	}
	require.NotNil(t, testProfile)
	require.Equal(t, fleet.MDMDeliveryPending, *testProfile.Status) // Should be NULL (pending for the user)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		var status *fleet.MDMDeliveryStatus
		err := sqlx.GetContext(t.Context(), q, &status, `SELECT status FROM host_mdm_apple_profiles WHERE profile_identifier = ?`, testProfile.Identifier)
		require.Nil(t, status)
		return err
	})

	// Acknowledge the original install command now, simulating the device response
	cmd, err = mdmDevice.Acknowledge(profileCmdID)
	require.NoError(t, err)

	// Now check if we see any new TestProfile cmds (we need to ack them here, since we might have skipped some above.)
	seenProfile = false
	for cmd != nil {
		if cmd.Command.RequestType != "InstallProfile" {
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
			continue
		}
		var fullCmd micromdm.CommandPayload
		err = plist.Unmarshal(cmd.Raw, &fullCmd)
		require.NoError(t, err)
		if strings.Contains(string(fullCmd.Command.InstallProfile.Payload), "TestProfile") {
			seenProfile = true
			// Acknowledge the resend command
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
		} else {
			// Acknowledge other commands
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
		}
	}
	require.Nil(t, cmd, "No further commands should be pending after acknowledging install")
	require.False(t, seenProfile, "No resend install command for TestProfile should be sent due to race condition")

	// Verify the profile is still in pending status (null in the DB)
	// aka. no race condition.
	hostProfiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)

	for _, p := range hostProfiles {
		if p.Identifier == "com.test.profile" {
			testProfile = &p
			break
		}
	}
	require.NotNil(t, testProfile)
	require.Equal(t, fleet.MDMDeliveryPending, *testProfile.Status)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		var status *fleet.MDMDeliveryStatus
		err := sqlx.GetContext(t.Context(), q, &status, `SELECT status FROM host_mdm_apple_profiles WHERE profile_identifier = ?`, testProfile.Identifier)
		require.Nil(t, status)
		return err
	})

	// run reconciler to resend any pending profiles
	s.awaitTriggerProfileSchedule(t)
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	// Now we should see the resend command
	seenProfile = false
	for cmd != nil {
		if cmd.Command.RequestType != "InstallProfile" {
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
			continue
		}
		var fullCmd micromdm.CommandPayload
		err = plist.Unmarshal(cmd.Raw, &fullCmd)
		require.NoError(t, err)
		if strings.Contains(string(fullCmd.Command.InstallProfile.Payload), "TestProfile") {
			seenProfile = true
			// Acknowledge the resend command
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
		} else {
			// Acknowledge other commands
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
		}
	}
	require.True(t, seenProfile, "Resend install command for TestProfile should be sent after reconciler runs")

	// And we should now also see the profile being marked as verifying
	hostProfiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	for _, p := range hostProfiles {
		if p.Identifier == "com.test.profile" {
			testProfile = &p
			break
		}
	}
	require.NotNil(t, testProfile)
	require.Equal(t, fleet.MDMDeliveryVerifying, *testProfile.Status)
}
